New command !mjolnir since <date or duration> <kick | ban | show> <limit> [reason] [...rooms] (#238)

A new command `since` to affect all users who have joined a protected room since a given date.
This commit is contained in:
David Teller 2022-03-21 10:39:15 +01:00 committed by GitHub
parent 814e528eaf
commit e05616b327
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 757 additions and 198 deletions

View File

@ -20,9 +20,11 @@
"@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/jsdom": "^16.2.11",
"@types/mocha": "^9.0.0",
"@types/node": "^16.7.10",
"@types/shell-quote": "1.7.1",
"crypto-js": "^4.1.1",
"eslint": "^7.32",
"expect": "^27.0.6",
@ -36,11 +38,13 @@
"config": "^3.3.6",
"express": "^4.17",
"html-to-text": "^8.0.0",
"humanize-duration": "^3.27.1",
"humanize-duration-ts": "^2.1.1",
"js-yaml": "^4.1.0",
"jsdom": "^16.6.0",
"matrix-bot-sdk": "^0.5.19",
"parse-duration": "^1.0.2"
"parse-duration": "^1.0.2",
"shell-quote": "^1.7.3"
},
"engines": {
"node": ">=14.0.0"

View File

@ -604,6 +604,12 @@ export class Mjolnir {
return list;
}
/**
* Get a protection by name.
*
* @return If there is a protection with this name *and* it is enabled,
* return the protection.
*/
public getProtection(protectionName: string): Protection | null {
return this.protections.get(protectionName) ?? null;
}

View File

@ -38,6 +38,8 @@ import { execShutdownRoomCommand } from "./ShutdownRoomCommand";
import { execAddAliasCommand, execMoveAliasCommand, execRemoveAliasCommand, execResolveCommand } from "./AliasCommands";
import { execKickCommand } from "./KickCommand";
import { execMakeRoomAdminCommand } from "./MakeRoomAdminCommand";
import { parse as tokenize } from "shell-quote";
import { execSinceCommand } from "./SinceCommand";
export const COMMAND_PREFIX = "!mjolnir";
@ -46,6 +48,10 @@ export async function handleCommand(roomId: string, event: { content: { body: st
const cmd = event['content']['body'];
const parts = cmd.trim().split(' ').filter(p => p.trim().length > 0);
// A shell-style parser that can parse `"a b c"` (with quotes) as a single argument.
// We do **not** want to parse `#` as a comment start, though.
const tokens = tokenize(cmd.replace("#", "\\#")).slice(/* get rid of ["!mjolnir", command] */ 2);
try {
if (parts.length === 1 || parts[1] === 'status') {
return await execStatusCommand(roomId, event, mjolnir, parts.slice(2));
@ -109,6 +115,8 @@ export async function handleCommand(roomId: string, event: { content: { body: st
return await execSetPowerLevelCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'shutdown' && parts[2] === 'room' && parts.length > 3) {
return await execShutdownRoomCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'since') {
return await execSinceCommand(roomId, event, mjolnir, tokens);
} else if (parts[1] === 'kick' && parts.length > 2) {
return await execKickCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'make' && parts[2] === 'admin' && parts.length > 3) {
@ -149,6 +157,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st
"!mjolnir alias add <room alias> <target room alias/ID> - Adds <room alias> to <target room>\n" +
"!mjolnir alias remove <room alias> - Deletes the room alias from whatever room it is attached to\n" +
"!mjolnir resolve <room alias> - Resolves a room alias to a room ID\n" +
"!mjolnir since <date>/<duration> <action> <limit> [rooms...] [reason] - Apply an action (kick, ban or just show) to all users who joined a room since a given date (up to <limit> users)\n" +
"!mjolnir shutdown room <room alias/ID> [message] - Uses the bot's account to shut down a room, preventing access to the room on this server\n" +
"!mjolnir powerlevel <user ID> <power level> [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms)\n" +
"!mjolnir make admin <room alias> [user alias/ID] - Make the specified user or the bot itself admin of the room\n" +

View File

@ -18,7 +18,7 @@ import { Mjolnir } from "../Mjolnir";
import { LogLevel } from "matrix-bot-sdk";
import config from "../config";
// !mjolnir kick <user> [room] [reason]
// !mjolnir kick <user|filter> [room] [reason]
export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const userId = parts[2];

View File

@ -0,0 +1,285 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService, RichReply } from "matrix-bot-sdk";
import { htmlEscape, parseDuration } from "../utils";
import { ParseEntry } from "shell-quote";
import { HumanizeDurationLanguage, HumanizeDuration } from "humanize-duration-ts";
const HUMANIZE_LAG_SERVICE: HumanizeDurationLanguage = new HumanizeDurationLanguage();
const HUMANIZER: HumanizeDuration = new HumanizeDuration(HUMANIZE_LAG_SERVICE);
enum Action {
Kick = "kick",
Ban = "ban",
Show = "show"
}
type Result<T> = {ok: T} | {error: string};
/**
* Attempt to parse a `ParseEntry`, as provided by the shell-style parser, using a parsing function.
*
* @param name The name of the object being parsed. Used for error messages.
* @param token The `ParseEntry` provided by the shell-style parser. It will be converted
* to string if possible. Otherwise, this returns an error.
* @param parser A function that attempts to parse `token` (converted to string) into
* its final result. It should provide an error fit for the end-user if it fails.
* @returns An error fit for the end-user if `token` could not be converted to string or
* if `parser` failed.
*/
function parseToken<T>(name: string, token: ParseEntry, parser: (source: string) => Result<T>): Result<T> {
if (!token) {
return { error: `Missing ${name}`};
}
if (typeof token === "object") {
if ("pattern" in token) {
// In future versions, we *might* be smarter about patterns, but not yet.
token = token.pattern;
}
}
if (typeof token !== "string") {
return { error: `Invalid ${name}` };
}
const result = parser(token);
if ("error" in result) {
if (result.error) {
return { error: `Invalid ${name} ${htmlEscape(token)}: ${result.error}`};
} else {
return { error: `Invalid ${name} ${htmlEscape(token)}`};
}
}
return result;
}
/**
* Attempt to convert a token into a string.
* @param name The name of the object being parsed. Used for error messages.
* @param token The `ParseEntry` provided by the shell-style parser. It will be converted
* to string if possible. Otherwise, this returns an error.
* @returns An error fit for the end-user if `token` could not be converted to string, otherwise
* `{ok: string}`.
*/
function getTokenAsString(name: string, token: ParseEntry): {error: string}|{ok: string} {
if (!token) {
return { error: `Missing ${name}`};
}
if (typeof token === "object" && "pattern" in token) {
// In future versions, we *might* be smarter patterns, but not yet.
token = token.pattern;
}
if (typeof token === "string") {
return {ok: token};
}
return { error: `Invalid ${name}` };
}
// !mjolnir since <date>/<duration> <action> <number> [...rooms] [...reason]
export async function execSinceCommand(destinationRoomId: string, event: any, mjolnir: Mjolnir, tokens: ParseEntry[]) {
let result = await execSinceCommandAux(destinationRoomId, event, mjolnir, tokens);
if ("error" in result) {
mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '❌');
mjolnir.logMessage(LogLevel.WARN, "SinceCommand", result.error);
const reply = RichReply.createFor(destinationRoomId, event, result.error, htmlEscape(result.error));
reply["msgtype"] = "m.notice";
/* no need to await */ mjolnir.client.sendMessage(destinationRoomId, reply);
} else {
// Details have already been printed.
mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '✅');
}
}
// Implementation of `execSinceCommand`, counts on caller to print errors.
//
// This method:
// - decodes all the arguments;
// - resolves any room alias into a room id;
// - attempts to execute action;
// - in case of success, returns `{ok: undefined}`, in case of error, returns `{error: string}`.
async function execSinceCommandAux(destinationRoomId: string, event: any, mjolnir: Mjolnir, tokens: ParseEntry[]): Promise<Result<undefined>> {
const [dateOrDurationToken, actionToken, maxEntriesToken, ...optionalTokens] = tokens;
// Parse origin date or duration.
const minDateResult = parseToken("<date>/<duration>", dateOrDurationToken, source => {
// Attempt to parse `<date>/<duration>` as a date.
let maybeMinDate = new Date(source);
let maybeMaxAgeMS = Date.now() - maybeMinDate.getTime() as number;
if (!Number.isNaN(maybeMaxAgeMS)) {
return { ok: { minDate: maybeMinDate, maxAgeMS: maybeMaxAgeMS} };
}
//...or as a duration
maybeMaxAgeMS = parseDuration(source);
if (maybeMaxAgeMS && !Number.isNaN(maybeMaxAgeMS)) {
maybeMaxAgeMS = Math.abs(maybeMaxAgeMS);
return { ok: { minDate: new Date(Date.now() - maybeMaxAgeMS), maxAgeMS: maybeMaxAgeMS } }
}
return { error: "" };
});
if ("error" in minDateResult) {
return minDateResult;
}
const { minDate, maxAgeMS } = minDateResult.ok!;
// Parse max entries.
const maxEntriesResult = parseToken("<maxEntries>", maxEntriesToken, source => {
const maybeMaxEntries = Number.parseInt(source, 10);
if (Number.isNaN(maybeMaxEntries)) {
return { error: "Not a number" };
} else {
return { ok: maybeMaxEntries };
}
});
if ("error" in maxEntriesResult) {
return maxEntriesResult;
}
const maxEntries = maxEntriesResult.ok!;
// Attempt to parse `<action>` as Action.
const actionResult = parseToken("<action>", actionToken, source => {
for (let key in Action) {
const maybeAction = Action[key as keyof typeof Action];
if (key === source) {
return { ok: maybeAction }
} else if (maybeAction === source) {
return { ok: maybeAction }
}
}
return {error: `Expected one of ${JSON.stringify(Action)}`};
})
if ("error" in actionResult) {
return actionResult;
}
const action: Action = actionResult.ok!;
// Now list affected rooms.
const rooms: Set</* room id */string> = new Set();
let reasonParts: string[] | undefined;
for (let token of optionalTokens) {
const maybeArg = getTokenAsString(reasonParts ? "[reason]" : "[room]", token);
if ("error" in maybeArg) {
return maybeArg;
}
const maybeRoom = maybeArg.ok;
if (!reasonParts) {
// If we haven't reached the reason yet, attempt to use `maybeRoom` as a room.
if (maybeRoom === "*") {
for (let roomId of Object.keys(mjolnir.protectedRooms)) {
rooms.add(roomId);
}
continue;
} else if (maybeRoom.startsWith("#") || maybeRoom.startsWith("!")) {
const roomId = await mjolnir.client.resolveRoom(maybeRoom);
if (!(roomId in mjolnir.protectedRooms)) {
return mjolnir.logMessage(LogLevel.WARN, "SinceCommand", `This room is not protected: ${htmlEscape(roomId)}.`);
}
rooms.add(roomId);
continue;
}
// If we reach this step, it's not a room, so it must be a reason.
// All further arguments are now part of `reason`.
reasonParts = [];
}
reasonParts.push(maybeRoom);
}
if (rooms.size === 0) {
return {
error: "Missing rooms. Use `*` if you wish to apply to every protected room.",
};
}
const progressEventId = await mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '⏳');
const reason: string | undefined = reasonParts?.join(" ");
for (let targetRoomId of rooms) {
let {html, text} = await (async () => {
switch (action) {
case Action.Show: {
return makeJoinStatus(mjolnir, targetRoomId, maxEntries, minDate, maxAgeMS);
}
case Action.Kick: {
const joins = mjolnir.roomJoins.getUsersInRoom(targetRoomId, minDate, maxEntries);
let results = { good: 0, bad: 0};
for (let join of joins) {
try {
await mjolnir.client.kickUser(join.userId, targetRoomId, reason);
results.good += 1;
} catch (ex) {
LogService.warn("SinceCommand", "Error while attempting to kick user", ex);
results.bad += 1;
}
}
const text_ = `Attempted to kick ${joins.length} users from room ${targetRoomId}, ${results.good} kicked, ${results.bad} failures`;
return {
html: text_,
text: text_,
}
}
case Action.Ban: {
const joins = mjolnir.roomJoins.getUsersInRoom(targetRoomId, minDate, maxEntries);
let results = { good: 0, bad: 0};
for (let join of joins) {
try {
await mjolnir.client.banUser(join.userId, targetRoomId, reason);
results.good += 1;
} catch (ex) {
LogService.warn("SinceCommand", "Error while attempting to ban user", ex);
results.bad += 1;
}
}
const text_ = `Attempted to ban ${joins.length} users from room ${targetRoomId}, ${results.good} kicked, ${results.bad} failures`;
return {
html: text_,
text: text_
}
}
}
})();
const reply = RichReply.createFor(destinationRoomId, event, text, html);
reply["msgtype"] = "m.notice";
/* no need to await */ mjolnir.client.sendMessage(destinationRoomId, reply);
}
await mjolnir.client.redactEvent(destinationRoomId, progressEventId);
return {ok: undefined};
}
function makeJoinStatus(mjolnir: Mjolnir, targetRoomId: string, maxEntries: number, minDate: Date, maxAgeMS: number): {html: string, text: string} {
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);
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(`<li>${htmlEscape(join.userId)}: ${durationHumanReadable}</li>`);
textFragments.push(`- ${join.userId}: ${durationHumanReadable}`);
}
return {
html: `${joins.length} recent joins (cut at ${maxAgeHumanReadable} ago / ${maxEntries} entries): <ul> ${htmlFragments.join()} </ul>`,
text: `${joins.length} recent joins (cut at ${maxAgeHumanReadable} ago / ${maxEntries} entries):\n${textFragments.join("\n")}`
}
}

View File

@ -16,17 +16,9 @@ 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 { htmlEscape, parseDuration } from "../utils";
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);
@ -36,10 +28,10 @@ export async function execStatusCommand(roomId: string, event: any, mjolnir: Mjo
case undefined:
case 'mjolnir':
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));
case 'protection':
return showProtectionStatus(roomId, event, mjolnir, parts.slice(/* ["protection"] */ 1));
default:
throw new Error(`Invalid status command: ${htmlEscape(parts[0])}`);
}

View File

@ -30,6 +30,18 @@ import {
import { Mjolnir } from "./Mjolnir";
import config from "./config";
import { ClientRequest, IncomingMessage } from "http";
import { default as parseDuration } from "parse-duration";
// 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"];
// ... and reexport it
export { parseDuration };
export function htmlEscape(input: string): string {
return input.replace(/["&<>]/g, (char: string) => ({

View File

@ -10,9 +10,25 @@ import * as crypto from "crypto";
* @param targetEventThunk A function that produces an event ID when called. This event ID is then used to listen for a reply.
* @returns The replying event.
*/
export async function getFirstReply(client: MatrixClient, targetRoom: string, targetEventThunk: () => Promise<string>): Promise<any> {
let reactionEvents = [];
const addEvent = function (roomId, event) {
export async function getFirstReply(client: MatrixClient, targetRoom: string, targetEventThunk: () => Promise<string>): Promise<any> {
return getNthReply(client, targetRoom, 1, targetEventThunk);
}
/**
* Returns a promise that resolves to the nth event replying to the event produced by targetEventThunk.
* @param client A MatrixClient that is already in the targetRoom. We will use it to listen for the event produced by targetEventThunk.
* This function assumes that the start() has already been called on the client.
* @param targetRoom The room to listen for the reply in.
* @param n The number of events to wait for. Must be >= 1.
* @param targetEventThunk A function that produces an event ID when called. This event ID is then used to listen for a reply.
* @returns The replying event.
*/
export async function getNthReply(client: MatrixClient, targetRoom: string, n: number, targetEventThunk: () => Promise<string>): Promise<any> {
if (Number.isNaN(n) || !Number.isInteger(n) || n <= 0) {
throw new TypeError(`Invalid number of events ${n}`);
}
let reactionEvents: any[] = [];
const addEvent = function (roomId: string, event: any) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.room.message') return;
reactionEvents.push(event);
@ -27,16 +43,22 @@ import * as crypto from "crypto";
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) {
return event;
n -= 1;
if (n === 0) {
return event;
}
}
}
return await new Promise(resolve => {
targetCb = function(roomId, event) {
targetCb = function(roomId: string, event: any) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.room.message') return;
const in_reply_to = event.content['m.relates_to']?.['m.in_reply_to'];
if (in_reply_to?.event_id === targetEventId) {
resolve(event)
n -= 1;
if (n === 0) {
resolve(event);
}
}
}
client.on('room.event', targetCb);
@ -50,7 +72,6 @@ import * as crypto from "crypto";
}
/**
* Returns a promise that resolves to an event that is reacting to the event produced by targetEventThunk.
* @param client A MatrixClient that is already in the targetRoom that can be started to listen for the event produced by targetEventThunk.
@ -61,8 +82,8 @@ import * as crypto from "crypto";
* @returns The reaction event.
*/
export async function getFirstReaction(client: MatrixClient, targetRoom: string, reactionKey: string, targetEventThunk: () => Promise<string>): Promise<any> {
let reactionEvents = [];
const addEvent = function (roomId, event) {
let reactionEvents: any[] = [];
const addEvent = function (roomId: string, event: any) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.reaction') return;
reactionEvents.push(event);
@ -78,7 +99,7 @@ export async function getFirstReaction(client: MatrixClient, targetRoom: string,
}
}
return await new Promise((resolve, reject) => {
targetCb = function(roomId, event) {
targetCb = function(roomId: string, event: any) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.reaction') return;
const relates_to = event.content['m.relates_to'];

View File

@ -1,8 +1,8 @@
import { strict as assert } from "assert";
import { SynapseRoomProperty } from "matrix-bot-sdk";
import { randomUUID } from "crypto";
import { RoomMemberManager } from "../../src/RoomMembers";
import { newTestUser } from "./clientHelper";
import { getFirstReply } from "./commands/commandUtils";
import { getFirstReply, getNthReply } from "./commands/commandUtils";
describe("Test: Testing RoomMemberManager", function() {
it("RoomMemberManager counts correctly when we call handleEvent manually", function() {
@ -16,8 +16,8 @@ describe("Test: Testing RoomMemberManager", function() {
manager.addRoom(room);
}
let joinDate = i => new Date(start.getTime() + i * 100_000);
let userId = i => `@sender_${i}:localhost`;
let joinDate = (i: number) => new Date(start.getTime() + i * 100_000);
let userId = (i: number) => `@sender_${i}:localhost`;
// First, add a number of joins.
const SAMPLE_SIZE = 100;
@ -42,7 +42,7 @@ describe("Test: Testing RoomMemberManager", function() {
for (let i = 0; i < SAMPLE_SIZE; ++i) {
const user = userId(i);
let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId;
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`);
@ -54,7 +54,7 @@ describe("Test: Testing RoomMemberManager", function() {
}
// Now, let's add a few leave events.
let leaveDate = i => new Date(start.getTime() + (SAMPLE_SIZE + i) * 100_000);
let leaveDate = (i: number) => new Date(start.getTime() + (SAMPLE_SIZE + i) * 100_000);
for (let i = 0; i < SAMPLE_SIZE / 3; ++i) {
const user = userId(i * 3);
@ -85,8 +85,8 @@ describe("Test: Testing RoomMemberManager", function() {
for (let i = 0; i < SAMPLE_SIZE; ++i) {
const user = userId(i);
let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId;
let isStillJoined = i % 3 != 0;
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}`);
@ -102,7 +102,7 @@ describe("Test: Testing RoomMemberManager", function() {
}
// Now let's make a few of these users rejoin.
let rejoinDate = i => new Date(start.getTime() + (SAMPLE_SIZE * 2 + i) * 100_000);
let rejoinDate = (i: number) => new Date(start.getTime() + (SAMPLE_SIZE * 2 + i) * 100_000);
for (let i = 0; i < SAMPLE_SIZE / 9; ++i) {
const user = userId(i * 9);
@ -133,9 +133,9 @@ describe("Test: Testing RoomMemberManager", function() {
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;
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}`);
@ -164,8 +164,8 @@ describe("Test: Testing RoomMemberManager", function() {
for (let i = 0; i < SAMPLE_SIZE; ++i) {
const user = userId(i);
let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId;
let hasRejoined = i % 9 == 0;
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}`);
@ -195,9 +195,9 @@ describe("Test: Testing RoomMemberManager", function() {
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;
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}`);
@ -226,8 +226,8 @@ describe("Test: Testing RoomMemberManager", function() {
for (let i = 0; i < SAMPLE_SIZE; ++i) {
const user = userId(i);
let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId;
let hasRejoined = i % 9 == 0;
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}`);
@ -245,8 +245,8 @@ describe("Test: Testing RoomMemberManager", function() {
afterEach(async function() {
await this.moderator?.stop();
if (this.users) {
for (let client of this.users) {
for (let array of [this.users, this.goodUsers, this.badUsers]) {
for (let client of array || []) {
await client.stop();
}
}
@ -303,8 +303,6 @@ describe("Test: Testing RoomMemberManager", function() {
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, () => {
@ -332,7 +330,7 @@ describe("Test: Testing RoomMemberManager", function() {
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) {
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}`);
@ -345,10 +343,10 @@ describe("Test: Testing RoomMemberManager", function() {
for (let i = 0; i < SAMPLE_SIZE / 2; ++i) {
const roomId = roomIds[i % roomIds.length];
const userId = userIds[i];
if (i % 3 == 0) {
if (i % 3 === 0) {
await this.moderator.kickUser(userId, roomId);
removedUsers.add(userIds[i]);
} else if (i % 3 == 1) {
} else if (i % 3 === 1) {
await this.moderator.banUser(userId, roomId);
removedUsers.add(userId);
}
@ -358,7 +356,6 @@ describe("Test: Testing RoomMemberManager", function() {
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 });
@ -366,7 +363,7 @@ describe("Test: Testing RoomMemberManager", function() {
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)) {
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}`);
@ -374,4 +371,229 @@ describe("Test: Testing RoomMemberManager", function() {
}
}
});
});
it("!mjolnir since kicks the correct users", async function() {
this.timeout(600_000);
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.
this.goodUsers = [];
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` } }));
}
const goodUserIds: string[] = [];
const badUserIds: string[] = [];
for (let client of this.goodUsers) {
goodUserIds.push(await client.getUserId());
}
for (let client of this.badUsers) {
badUserIds.push(await client.getUserId());
}
// Create and protect rooms.
// - room 0 remains unprotected, as witness;
// - room 1 is protected but won't be targeted directly, also as witness.
const NUMBER_OF_ROOMS = 14;
const roomIds: string[] = [];
const roomAliases: string[] = [];
const mjolnirUserId = await this.mjolnir.client.getUserId();
for (let i = 0; i < NUMBER_OF_ROOMS; ++i) {
const roomId = await this.moderator.createRoom({
invite: [mjolnirUserId, ...goodUserIds, ...badUserIds],
});
roomIds.push(roomId);
const alias = `#since-test-${randomUUID()}:localhost:9999`;
await this.moderator.createRoomAlias(alias, roomId);
roomAliases.push(alias);
}
for (let i = 1; i < roomIds.length; ++i) {
// Protect all rooms except roomIds[0], as witness.
const roomId = roomIds[i];
await this.mjolnir.client.joinRoom(roomId);
await this.moderator.setUserPowerLevel(mjolnirUserId, roomId, 100);
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 i = 1; i < roomIds.length; ++i) {
const roomId = roomIds[i];
if (!(roomId in protectedRooms)) {
protectedRoomsUpdated = false;
await new Promise(resolve => setTimeout(resolve, 1_000));
}
}
} while (!protectedRoomsUpdated);
// Good users join before cut date.
for (let user of this.goodUsers) {
for (let roomId of roomIds) {
await user.joinRoom(roomId);
}
}
await new Promise(resolve => setTimeout(resolve, 5_000));
const cutDate = new Date();
await new Promise(resolve => setTimeout(resolve, 5_000));
// Bad users join after cut date.
for (let user of this.badUsers) {
for (let roomId of roomIds) {
await user.joinRoom(roomId);
}
}
enum Method {
kick,
ban
}
const WITNESS_UNPROTECTED_ROOM_ID = roomIds[0];
const WITNESS_ROOM_ID = roomIds[1];
const EXPERIMENTS = [
// Kick bad users in one room, using duration syntax, no reason.
{
// A human-readable name for the command.
name: "kick with duration",
// The actual command-line.
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomId}`,
// If `true`, this command should affect room `WITNESS_ROOM_ID`.
shouldAffectWitnessRoom: false,
// The number of responses we expect to this command.
n: 1,
// How affected users should leave the room.
method: Method.kick,
},
// Ban bad users in one room, using duration syntax, no reason.
{
name: "ban with duration",
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms ban 100 ${roomId}`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.ban,
},
// Kick bad users in one room, using date syntax, no reason.
{
name: "kick with date",
command: (roomId: string) => `!mjolnir since "${cutDate}" kick 100 ${roomId}`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.kick,
},
// Ban bad users in one room, using date syntax, no reason.
{
name: "ban with date",
command: (roomId: string) => `!mjolnir since "${cutDate}" ban 100 ${roomId}`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.ban,
},
// Kick bad users in one room, using duration syntax, with reason.
{
name: "kick with duration and reason",
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomId} bad, bad user`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.kick,
},
// Ban bad users in one room, using duration syntax, with reason.
{
name: "ban with duration and reason",
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms ban 100 ${roomId} bad, bad user`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.ban,
},
// Kick bad users in one room, using date syntax, with reason.
{
name: "kick with date and reason",
command: (roomId: string) => `!mjolnir since "${cutDate}" kick 100 ${roomId} bad, bad user`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.kick,
},
// Ban bad users in one room, using date syntax, with reason.
{
name: "ban with date and reason",
command: (roomId: string) => `!mjolnir since "${cutDate}" ban 100 ${roomId} bad, bad user`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.ban,
},
// Kick bad users in one room, using duration syntax, without reason, using alias.
{
name: "kick with duration, no reason, alias",
command: (_: string, roomAlias: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomAlias}`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.kick,
},
// Kick bad users in one room, using duration syntax, with reason, using alias.
{
name: "kick with duration, reason and alias",
command: (_: string, roomAlias: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomAlias} for some reason`,
shouldAffectWitnessRoom: false,
n: 1,
method: Method.kick,
},
// Kick bad users everywhere, no reason
{
name: "kick with date everywhere",
command: () => `!mjolnir since "${cutDate}" kick 100 * bad, bad user`,
shouldAffectWitnessRoom: true,
n: NUMBER_OF_ROOMS - 1,
method: Method.kick,
}
];
for (let i = 0; i < EXPERIMENTS.length; ++i) {
const experiment = EXPERIMENTS[i];
const roomId = roomIds[i + 2];
const roomAlias = roomAliases[i + 2];
const joined = this.mjolnir.roomJoins.getUsersInRoom(roomId, start, 100);
assert.ok(joined.length >= 2 * SAMPLE_SIZE, `We should have seen ${2 * SAMPLE_SIZE} users, saw ${joined.length}`);
await getNthReply(this.mjolnir.client, this.mjolnir.managementRoomId, experiment.n, async () => {
const command = experiment.command(roomId, roomAlias);
let result = await this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command });
return result;
});
const usersInRoom = await this.mjolnir.client.getJoinedRoomMembers(roomId);
const usersInUnprotectedWitnessRoom = await this.mjolnir.client.getJoinedRoomMembers(WITNESS_UNPROTECTED_ROOM_ID);
const usersInWitnessRoom = await this.mjolnir.client.getJoinedRoomMembers(WITNESS_ROOM_ID);
for (let userId of goodUserIds) {
assert.ok(usersInRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in affected room`);
assert.ok(usersInWitnessRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in witness room`);
assert.ok(usersInUnprotectedWitnessRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in unprotected witness room`);
}
for (let userId of badUserIds) {
assert.ok(!usersInRoom.includes(userId), `After a ${experiment.name}, bad user ${userId} should NOT be in affected room`);
assert.equal(usersInWitnessRoom.includes(userId), !experiment.shouldAffectWitnessRoom, `After a ${experiment.name}, bad user ${userId} should ${experiment.shouldAffectWitnessRoom ? "NOT" : "still"} be in witness room`);
assert.ok(usersInUnprotectedWitnessRoom.includes(userId), `After a ${experiment.name}, bad user ${userId} should still be in unprotected witness room`);
const leaveEvent = await this.mjolnir.client.getRoomStateEvent(roomId, "m.room.member", userId);
switch (experiment.method) {
case Method.kick:
assert.equal(leaveEvent.membership, "leave");
break;
case Method.ban:
assert.equal(leaveEvent.membership, "ban");
break;
}
}
}
});
});

View File

@ -20,6 +20,7 @@
},
"include": [
"./src/**/*",
"./test/integration/manualLaunchScript.ts"
"./test/integration/manualLaunchScript.ts",
"./test/integration/roomMembersTest.ts"
]
}

297
yarn.lock
View File

@ -70,11 +70,6 @@
"@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"
@ -88,14 +83,6 @@
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"
@ -130,7 +117,7 @@
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express@^4.17.13":
"@types/express@^4.17.7":
version "4.17.13"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
@ -145,6 +132,11 @@
resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-8.0.1.tgz#e449513df2283b1adedc85bdc2f6b7187f32972a"
integrity sha512-0B/OifmJYmk5r9z9+KJtGWOF0LEjbTN4D2QeCh+mAw81JkJwC83NvNWUZFEqRT5PpnjX7vX0ab1SMGcwCs3Lag==
"@types/humanize-duration@^3.27.1":
version "3.27.1"
resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7"
integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@ -216,6 +208,11 @@
"@types/mime" "^1"
"@types/node" "*"
"@types/shell-quote@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.7.1.tgz#2d059091214a02c29f003f591032172b2aff77e8"
integrity sha512-SWZ2Nom1pkyXCDohRSrkSKvDh8QOG9RfAsrt5/NsPQC4UQJ55eG0qClA40I+Gkez4KTQ0uDUT8ELRXThf3J5jw==
"@types/stack-utils@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
@ -256,6 +253,14 @@ 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"
@ -311,11 +316,6 @@ 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"
@ -452,20 +452,20 @@ 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==
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.1"
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.6"
raw-body "2.4.2"
qs "6.9.7"
raw-body "2.4.3"
type-is "~1.6.18"
brace-expansion@^1.1.7:
@ -508,10 +508,10 @@ 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==
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"
@ -537,7 +537,7 @@ chalk@^2.0.0, chalk@^2.3.0:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4, chalk@^4.0.0, chalk@^4.1.0:
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==
@ -651,10 +651,10 @@ 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==
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"
@ -815,6 +815,13 @@ 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"
@ -822,7 +829,7 @@ domhandler@^4.0.0, domhandler@^4.2.0:
dependencies:
domelementtype "^2.2.0"
domutils@^2.5.2:
domutils@^2.0.0, domutils@^2.5.2:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
@ -1080,17 +1087,17 @@ express@^4.17:
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==
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.7"
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.19.1"
body-parser "1.19.2"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.4.1"
cookie "0.4.2"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
@ -1105,7 +1112,7 @@ express@^4.17.2:
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.7"
qs "6.9.6"
qs "6.9.7"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.17.2"
@ -1288,7 +1295,7 @@ glob@7.1.7:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.0, glob@^7.1.1, glob@^7.1.3:
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==
@ -1367,6 +1374,17 @@ 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"
@ -1379,23 +1397,21 @@ 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"
@ -1470,6 +1486,11 @@ humanize-duration-ts@^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==
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==
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -1513,11 +1534,6 @@ 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"
@ -1537,13 +1553,6 @@ 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"
@ -1758,6 +1767,11 @@ 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"
@ -1796,7 +1810,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.7.0:
lodash@4, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -1809,7 +1823,7 @@ log-symbols@4.1.0:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
lowdb@^1:
lowdb@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064"
integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==
@ -1840,25 +1854,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@file:../blurbs/matrix-bot-sdk":
version "99.0.2"
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==
dependencies:
"@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"
"@types/express" "^4.17.7"
chalk "^4.1.0"
express "^4.17.1"
glob-to-regexp "^0.4.1"
hash.js "^1.1.7"
html-to-text "^8.1.0"
html-to-text "^6.0.0"
htmlencode "^0.0.4"
lowdb "^1"
lowdb "^1.0.0"
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.6.1"
sanitize-html "^2.3.2"
media-typer@0.3.0:
version "0.3.0"
@ -1888,6 +1902,11 @@ 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"
@ -1895,6 +1914,13 @@ 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"
@ -1995,15 +2021,20 @@ 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.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==
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==
natural-compare@^1.4.0:
version "1.4.0"
@ -2025,6 +2056,11 @@ 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"
@ -2147,7 +2183,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.7:
path-parse@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
@ -2162,11 +2198,6 @@ 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"
@ -2177,14 +2208,14 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
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==
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==
dependencies:
nanoid "^3.2.0"
picocolors "^1.0.0"
source-map-js "^1.0.2"
nanocolors "^0.2.2"
nanoid "^3.1.25"
source-map-js "^0.6.2"
prelude-ls@^1.2.1:
version "1.2.1"
@ -2239,10 +2270,10 @@ 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.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"
@ -2284,12 +2315,12 @@ 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==
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.1"
bytes "3.1.2"
http-errors "1.8.1"
iconv-lite "0.4.24"
unpipe "1.0.0"
@ -2306,13 +2337,6 @@ 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"
@ -2376,15 +2400,6 @@ 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"
@ -2420,17 +2435,18 @@ safe-buffer@5.2.1, 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.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==
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==
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.3.11"
postcss "^8.0.2"
saxes@^5.0.1:
version "5.0.1"
@ -2545,14 +2561,10 @@ 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"
shell-quote@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
sigmund@^1.0.1:
version "1.0.1"
@ -2573,10 +2585,10 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
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-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-support@^0.5.6:
version "0.5.20"
@ -2682,11 +2694,6 @@ 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"