mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-05-14 03:15:11 +00:00
879e822332
* Move src to apps/draupnir/src https://github.com/the-draupnir-project/planning/issues/100 * Move package.json https://github.com/the-draupnir-project/planning/issues/100 * Add matrix-basic-types to monorepo. Get everything working including linting and prettier :3 https://github.com/the-draupnir-project/planning/issues/100 * Add interface-manager to monorepo. This was a bitch because apparently we forgot to delete node_modules before creating the workspace package.json. So it had linked a bunch of local stuff like was in node_modules for Draupnir... Anyways i think we're still on track. https://github.com/the-draupnir-project/planning/issues/100 * idk why there are prettier changes in apps but there are. * Add matrix-protection-suite to monorepo. https://github.com/the-draupnir-project/planning/issues/100 * Add matrix-protection-suite-for-matrix-bot-sdk https://github.com/the-draupnir-project/planning/issues/100 We will need to add the real upstreams and versions and remove the file links as we publish the packages. * Move mps-interface-adaptor into monorepo https://github.com/the-draupnir-project/planning/issues/100 Wohoo, i think only draupnir is left now? * Move Draupnir test files to draupnir directory smh smh smh. https://github.com/the-draupnir-project/planning/issues/100 * Fix typescript config for tests and eslint. Now we get proper linting and type checking of tests. https://github.com/the-draupnir-project/planning/issues/100 * WIP Integrating draupnir into monorepo tooling... https://github.com/the-draupnir-project/planning/issues/100 We need to stop aliasing bot-sdk but we should first check that upstream is using a consistent name too. * Remove matrix-bot-sdk alias for vector fork. https://github.com/the-draupnir-project/planning/issues/100 * Add top command description type and weave through API. A more recent version of typescript meant that the exectutor's contravariance got checked which destroyed the API so we had to make a top type for command descriptions and parametrise some of the API. https://github.com/the-draupnir-project/planning/issues/100 * Fix typescript errors related to class property initialisation changes. https://www.typescriptlang.org/tsconfig/#useDefineForClassFields Seems like they were using defineProperty before which meant properites were initialised after the constructor ran. Honestly i like that more but we're going to stick with what they intend to be the default. https://github.com/the-draupnir-project/planning/issues/100 * Fix tests lacking fixtures context. https://github.com/the-draupnir-project/planning/issues/100 * Fix typescript errors related to error destructuring in tests. https://github.com/the-draupnir-project/planning/issues/100 * Pin postgres package to workaround upstream issue https://github.com/porsager/postgres/issues/1150 Documented in DEPENDENCIES.md https://github.com/the-draupnir-project/planning/issues/100 * Fix contravariance issue in hash store helper. Part of the TS 5.9 upgrade fallout. https://github.com/the-draupnir-project/planning/issues/100 * Fix minor typescript 5.9 migration issuess All typescript errors finished, yay. * Fix REUSE missing headers. * Fix assets script in draupnir app. * Add Draupnir to eslint scope * Remove the appservice web API. There are too many eslint errors here to do with unsafe parsing of properties from the body etc. And there's actually no consumers to this API. It's also a widget API, and all it does is provision the bot and nothing more. * Fix eslint config for DeadDocumentJSX. It wasn't working well with the jsx templates. We should probably delete the tsconfig.eslint.json shite now. * Update src/utils.ts for eslint. This shit is legacy i hate it. * Fix eslint errors in config. Really this is paint over rot since the config doesn't have a schema, and we can't really make one either. * Fix eslint issues in ReportManager. This code is diabolical. It hasn't really been fixed that will take refactoring and making sure people don't write this sorts of bad code ever again. Which thankfully we have process in place for. * Fix clientHelper eslint issues. * Fix eslint for ImportCommand. * Grinding eslint fml. * Fix miscellaneous eslint issues. * allow no-deprecate for logMessage. shit's being annoying. * Fix remaining eslint issues... We also deleted one of the scripts used to evaluate the performance of various endpoints, which we were not using. * Give bot toggle asyncDispose for code consistency. * Fix package.json access issues. * Adjust Docker and CI for new app location in monorepo. * Fix broken integration tests. * Remove prepare script from matrix-protection-suite package. Isn't needed anymore * Fix build:all script missing base files. * Remove test script from matrix-protection-suite-for-matrix-bot-sdk It doesn't have any tests :/ * Order of setup is wrong in integration test workflows. * Fix mps interface adaptor doesn't have any tests. * Fix appservice registration for test harness. * Fix matrix-basic-types jest configuration * Fix no build step in mjolnir.yaml * Transfer common dev dependencies to the workspace root. They were just wrong.
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
// Copyright 2022 - 2024 Gnuxie <Gnuxie@protonmail.com>
|
|
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
|
//
|
|
// SPDX-FileAttributionText: <text>
|
|
// This modified file incorporates work from mjolnir
|
|
// https://github.com/matrix-org/mjolnir
|
|
// </text>
|
|
|
|
import { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
import { strict as assert } from "assert";
|
|
import * as crypto from "crypto";
|
|
import {
|
|
MatrixSendClient,
|
|
SafeMatrixEmitter,
|
|
} from "matrix-protection-suite-for-matrix-bot-sdk";
|
|
import {
|
|
NoticeMessageContent,
|
|
ReactionEvent,
|
|
RoomEvent,
|
|
TextMessageContent,
|
|
Value,
|
|
StringEventIDSchema,
|
|
ReactionContent,
|
|
hasOwn,
|
|
} from "matrix-protection-suite";
|
|
import { Type } from "@sinclair/typebox";
|
|
import { Draupnir } from "../../../src/Draupnir";
|
|
import {
|
|
StringEventID,
|
|
StringRoomID,
|
|
} from "@the-draupnir-project/matrix-basic-types";
|
|
|
|
export const ReplyContent = Type.Intersect([
|
|
Type.Object({
|
|
"m.relates_to": Type.Object({
|
|
"m.in_reply_to": Type.Object({
|
|
event_id: StringEventIDSchema,
|
|
}),
|
|
}),
|
|
}),
|
|
Type.Union([NoticeMessageContent, TextMessageContent]),
|
|
]);
|
|
export type ReplyContent = typeof ReplyContent;
|
|
|
|
/**
|
|
* Returns a promise that resolves to the first event replying to the event produced by targetEventThunk.
|
|
* @param matrix A MatrixEmitter from 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 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(
|
|
matrix: SafeMatrixEmitter,
|
|
targetRoom: string,
|
|
targetEventThunk: () => Promise<string>
|
|
): Promise<RoomEvent<ReplyContent>> {
|
|
return getNthReply(matrix, targetRoom, 1, targetEventThunk);
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves to the nth event replying to the event produced by targetEventThunk.
|
|
* @param matrix A MatrixEmitter from 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(
|
|
matrix: SafeMatrixEmitter,
|
|
targetRoom: string,
|
|
n: number,
|
|
targetEventThunk: () => Promise<string>
|
|
): Promise<RoomEvent<ReplyContent>> {
|
|
if (Number.isNaN(n) || !Number.isInteger(n) || n <= 0) {
|
|
throw new TypeError(`Invalid number of events ${n}`);
|
|
}
|
|
const reactionEvents: RoomEvent[] = [];
|
|
const addEvent = function (roomId: string, event: RoomEvent) {
|
|
if (roomId !== targetRoom) return;
|
|
if (event.type !== "m.room.message") return;
|
|
reactionEvents.push(event);
|
|
};
|
|
let targetCb;
|
|
try {
|
|
matrix.on("room.event", addEvent);
|
|
const targetEventId = await targetEventThunk();
|
|
if (typeof targetEventId !== "string") {
|
|
throw new TypeError();
|
|
}
|
|
for (const event of reactionEvents) {
|
|
if (Value.Check(ReplyContent, event.content)) {
|
|
const in_reply_to = event.content["m.relates_to"]["m.in_reply_to"];
|
|
if (in_reply_to.event_id === targetEventId) {
|
|
n -= 1;
|
|
if (n === 0) {
|
|
return event as RoomEvent<ReplyContent>;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return await new Promise((resolve) => {
|
|
targetCb = function (roomId: string, event: RoomEvent) {
|
|
if (roomId !== targetRoom) return;
|
|
if (event.type !== "m.room.message") return;
|
|
if (Value.Check(ReplyContent, event.content)) {
|
|
const in_reply_to = event.content["m.relates_to"]["m.in_reply_to"];
|
|
if (in_reply_to.event_id === targetEventId) {
|
|
n -= 1;
|
|
if (n === 0) {
|
|
resolve(event as RoomEvent<ReplyContent>);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
matrix.on("room.event", targetCb);
|
|
});
|
|
} finally {
|
|
matrix.removeListener("room.event", addEvent);
|
|
// the type feedback for eslitn has to be wrong here i don't get it.
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
if (targetCb) {
|
|
matrix.removeListener("room.event", targetCb);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves to an event that is reacting to the event produced by targetEventThunk.
|
|
* @param matrix A MatrixEmitter for a MatrixClient that is already in the targetRoom that can be started 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 reaction in.
|
|
* @param reactionKey The reaction key to wait for.
|
|
* @param targetEventThunk A function that produces an event ID when called. This event ID is then used to listen for a reaction.
|
|
* @returns The reaction event.
|
|
*/
|
|
export async function getFirstReaction(
|
|
matrix: SafeMatrixEmitter,
|
|
targetRoom: string,
|
|
reactionKey: string,
|
|
targetEventThunk: () => Promise<string>
|
|
): Promise<ReactionEvent> {
|
|
const reactionEvents: ReactionEvent[] = [];
|
|
const addEvent = function (roomId: string, event: RoomEvent) {
|
|
if (roomId !== targetRoom) return;
|
|
if (!Value.Check(ReactionEvent, event)) return;
|
|
if (Object.keys(event.content).length === 0) return;
|
|
reactionEvents.push(event);
|
|
};
|
|
let targetCb;
|
|
try {
|
|
matrix.on("room.event", addEvent);
|
|
const targetEventId = await targetEventThunk();
|
|
for (const event of reactionEvents) {
|
|
const relates_to = (
|
|
hasOwn(event.content, "m.relates_to")
|
|
? event.content["m.relates_to"]
|
|
: undefined
|
|
) as ReactionContent["m.relates_to"];
|
|
if (
|
|
relates_to?.event_id === targetEventId &&
|
|
relates_to.key === reactionKey
|
|
) {
|
|
return event;
|
|
}
|
|
}
|
|
return await new Promise((resolve) => {
|
|
targetCb = function (roomId: string, event: RoomEvent) {
|
|
if (roomId !== targetRoom) return;
|
|
if (!Value.Check(ReactionEvent, event)) return;
|
|
const relates_to = (
|
|
hasOwn(event.content, "m.relates_to")
|
|
? event.content["m.relates_to"]
|
|
: undefined
|
|
) as ReactionContent["m.relates_to"];
|
|
if (
|
|
relates_to?.event_id === targetEventId &&
|
|
relates_to.key === reactionKey
|
|
) {
|
|
resolve(event);
|
|
}
|
|
};
|
|
matrix.on("room.event", targetCb);
|
|
});
|
|
} finally {
|
|
matrix.off("room.event", addEvent);
|
|
// idk why the type checker can't detect that this condition is necessary.
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
if (targetCb) {
|
|
matrix.off("room.event", targetCb);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get and wait for the first event that matches a predicate.
|
|
* @param details.lookAfterEvent A function that returns an event id to look for
|
|
* in the sync timeline before matching. Or a function that returns undefined if
|
|
* `getFirstEventMatching` should start matching events right away.
|
|
* @returns The event matching the predicate provided.
|
|
*/
|
|
export async function getFirstEventMatching(details: {
|
|
matrix: SafeMatrixEmitter;
|
|
targetRoom: string;
|
|
lookAfterEvent: () => Promise</*event id*/ string | undefined>;
|
|
predicate: (event: RoomEvent) => boolean;
|
|
}): Promise<RoomEvent> {
|
|
let targetCb;
|
|
try {
|
|
return await new Promise((resolve) => {
|
|
void details.lookAfterEvent().then((afterEventId: string | undefined) => {
|
|
// if the event has returned an event id, then we will wait for that in the timeline,
|
|
// otherwise the "event" isn't a matrix event and we just have to start looking right away.
|
|
let isAfterEventId = afterEventId === undefined;
|
|
targetCb = (roomId: string, event: RoomEvent) => {
|
|
if (event["event_id"] === afterEventId) {
|
|
isAfterEventId = true;
|
|
return;
|
|
}
|
|
if (isAfterEventId && details.predicate(event)) {
|
|
resolve(event);
|
|
}
|
|
};
|
|
details.matrix.on("room.event", targetCb);
|
|
});
|
|
});
|
|
} finally {
|
|
// unless i'm dumb, the type inference for this is wrong, and i don't know why.
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
if (targetCb) {
|
|
details.matrix.off("room.event", targetCb);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new banlist for draupnir to watch and return the shortcode that can be used to refer to the list in future commands.
|
|
* @param managementRoom The room to send the create command to.
|
|
* @param matrix A syncing matrix client.
|
|
* @param client A client that isn't draupnir to send the message with, as you will be invited to the room.
|
|
* @returns The shortcode for the list that can be used to refer to the list in future commands.
|
|
*/
|
|
export async function createBanList(
|
|
managementRoom: string,
|
|
matrix: SafeMatrixEmitter,
|
|
client: MatrixClient
|
|
): Promise<string> {
|
|
const listName = crypto.randomUUID();
|
|
const listCreationResponse = await getFirstReply(
|
|
matrix,
|
|
managementRoom,
|
|
async () => {
|
|
return await client.sendMessage(managementRoom, {
|
|
msgtype: "m.text",
|
|
body: `!draupnir list create ${listName} ${listName}`,
|
|
});
|
|
}
|
|
);
|
|
assert.equal(
|
|
listCreationResponse.content.body.includes(
|
|
"This list is now being watched."
|
|
),
|
|
true,
|
|
"could not create a list to test with."
|
|
);
|
|
return listName;
|
|
}
|
|
|
|
export async function sendCommand(
|
|
draupnir: Draupnir,
|
|
command: string
|
|
): Promise<{ eventID: StringEventID }> {
|
|
const eventID = (await draupnir.client.sendMessage(
|
|
draupnir.managementRoomID,
|
|
{
|
|
msgtype: "m.text",
|
|
body: command,
|
|
}
|
|
)) as StringEventID;
|
|
return { eventID };
|
|
}
|
|
|
|
export async function acceptPropmt(
|
|
client: MatrixSendClient,
|
|
roomID: StringRoomID,
|
|
eventID: StringEventID,
|
|
promptKey: string
|
|
): Promise<void> {
|
|
// i suspect that adding a reaction using the unstable API doesn't work because it uses the usntable prefix
|
|
// whereas our schema doesn't have the unstable event type.
|
|
// we don't test for this anywhere and we should really unify the situation between draupnir, draupnir safe mode,
|
|
// and any new bots that just need all the same things.
|
|
await client.unstableApis.addReactionToEvent(roomID, eventID, promptKey);
|
|
}
|