mjolnir/src/ManagementRoomOutput.ts
Gnuxie 704bb660c2
Refactor how we listen for matrix events. (#446)
* Refactor Matrix event listener in Mjolnir and ManagedMjolnir.

closes https://github.com/matrix-org/mjolnir/issues/411.

Issue #411 says that we have to be careful about room.join,
but this was before we figured how to make matrix-appservice-bridge
echo events sent by its own intents.

* Remove MatrixClientListener since it isn't actually needed.

* Protect which config values can be used for ManagedMjolnirs.

* Introduce MatrixSendClient

so listeners aren't accidentally added to a MatrixClient instead
of MatrixEmitter.

* doc

* Move provisioned mjolnir config to src/config.

This just aids maintance so whenever someone goes to change the config
of the bot they will see this and update it.

* doc for matrix intent listener.
2022-12-06 17:17:40 +00:00

134 lines
5.9 KiB
TypeScript

/*
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.
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 * as Sentry from "@sentry/node";
import { extractRequestError, LogLevel, LogService, MessageType, Permalinks, TextualMessageEventContent, UserID } from "matrix-bot-sdk";
import { IConfig } from "./config";
import { MatrixSendClient } from "./MatrixEmitter";
import { htmlEscape } from "./utils";
const levelToFn = {
[LogLevel.DEBUG.toString()]: LogService.debug,
[LogLevel.INFO.toString()]: LogService.info,
[LogLevel.WARN.toString()]: LogService.warn,
[LogLevel.ERROR.toString()]: LogService.error,
};
/**
* Allows the different componenets of mjolnir to send messages back to the management room without introducing a dependency on the entirity of a `Mjolnir` instance.
*/
export default class ManagementRoomOutput {
constructor(
private readonly managementRoomId: string,
private readonly client: MatrixSendClient,
private readonly config: IConfig,
) {
}
/**
* Take an arbitrary string and a set of room IDs, and return a
* TextualMessageEventContent whose plaintext component replaces those room
* IDs with their canonical aliases, and whose html component replaces those
* room IDs with their matrix.to room pills.
*
* @param client The matrix client on which to query for room aliases
* @param text An arbitrary string to rewrite with room aliases and pills
* @param roomIds A set of room IDs to find and replace in `text`
* @param msgtype The desired message type of the returned TextualMessageEventContent
* @returns A TextualMessageEventContent with replaced room IDs
*/
private async replaceRoomIdsWithPills(text: string, roomIds: Set<string>, msgtype: MessageType = "m.text"): Promise<TextualMessageEventContent> {
const content: TextualMessageEventContent = {
body: text,
formatted_body: htmlEscape(text),
msgtype: msgtype,
format: "org.matrix.custom.html",
};
// Though spec doesn't say so, room ids that have slashes in them are accepted by Synapse and Dendrite unfortunately for us.
const escapeRegex = (v: string): string => {
return v.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};
const viaServers = [(new UserID(await this.client.getUserId())).domain];
for (const roomId of roomIds) {
let alias = roomId;
try {
alias = (await this.client.getPublishedAlias(roomId)) || roomId;
} catch (e) {
// This is a recursive call, so tell the function not to try and call us
await this.logMessage(LogLevel.WARN, "utils", `Failed to resolve room alias for ${roomId} - see console for details`, null, true);
LogService.warn("utils", extractRequestError(e));
}
const regexRoomId = new RegExp(escapeRegex(roomId), "g");
content.body = content.body.replace(regexRoomId, alias);
if (content.formatted_body) {
const permalink = Permalinks.forRoom(alias, alias !== roomId ? [] : viaServers);
content.formatted_body = content.formatted_body.replace(regexRoomId, `<a href="${permalink}">${htmlEscape(alias)}</a>`);
}
}
return content;
}
/**
* Log a message to the management room and the console, replaces any room ids in additionalRoomIds with pills.
*
* @param level Used to determine whether to hide the message or not depending on `config.verboseLogging`.
* @param module Used to help find where in the source the message is coming from (when logging to the console).
* @param message The message we want to log.
* @param additionalRoomIds The roomIds in the message that we want to be replaced by room pills.
* @param isRecursive Whether logMessage is being called from logMessage.
*/
public async logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string | null = null, isRecursive = false): Promise<any> {
if (level === LogLevel.ERROR) {
Sentry.captureMessage(`${module}: ${message}`, 'error');
}
if (!additionalRoomIds) additionalRoomIds = [];
if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds];
if (this.config.verboseLogging || LogLevel.INFO.includes(level)) {
let clientMessage = message;
if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`;
if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`;
const client = this.client;
const roomIds = [this.managementRoomId, ...additionalRoomIds];
let evContent: TextualMessageEventContent = {
body: message,
formatted_body: htmlEscape(message),
msgtype: "m.notice",
format: "org.matrix.custom.html",
};
if (!isRecursive) {
evContent = await this.replaceRoomIdsWithPills(clientMessage, new Set(roomIds), "m.notice");
}
try {
await client.sendMessage(this.managementRoomId, evContent);
} catch (ex) {
// We want to be informed if we cannot log a message.
Sentry.captureException(ex);
}
}
levelToFn[level.toString()](module, message);
}
}