Add opentelemetry tracing support

Add logging to tracing

Improve logging

Fix order of imports

Add missing pieces of tracing code

Add more logging

Try nested spans

Expand traces using an decorator

Improve quality of spans

Add missing tracing decorations

Filter metrics and healthz

Add more traces

Fix return type error
This commit is contained in:
MTRNord
2023-07-19 15:00:44 +02:00
parent 40213a0794
commit fffd8563e3
55 changed files with 11846 additions and 525 deletions
+9328
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -67,4 +67,4 @@
"engines": {
"node": ">=18.0.0"
}
}
}
+4
View File
@@ -25,6 +25,8 @@ limitations under the License.
* are NOT distributed, contributed, committed, or licensed under the Apache License.
*/
import { traceSync } from "./utils";
export const ERROR_KIND_PERMISSION = "permission";
export const ERROR_KIND_FATAL = "fatal";
@@ -51,6 +53,7 @@ export default class ErrorCache {
* @param roomId The room to reset the error cache for.
* @param kind The kind of error we are resetting.
*/
@traceSync('ErrorCache.resetError')
public resetError(roomId: string, kind: string) {
if (!this.roomsToErrors.has(roomId)) {
this.roomsToErrors.set(roomId, new Map());
@@ -65,6 +68,7 @@ export default class ErrorCache {
* @returns True if the error kind has been triggered in that room,
* meaning it has been longer than the time specified in `TRIGGER_INTERVALS` since the last trigger (or the first trigger). Otherwise false.
*/
@traceSync('ErrorCache.triggerError')
public triggerError(roomId: string, kind: string): boolean {
if (!this.roomsToErrors.get(roomId)) {
this.roomsToErrors.set(roomId, new Map());
+2 -1
View File
@@ -30,7 +30,7 @@ import { LogLevel, LogService, MessageType, TextualMessageEventContent, UserID }
import { Permalinks } from "./commands/interface-manager/Permalinks";
import { IConfig } from "./config";
import { MatrixSendClient } from "./MatrixEmitter";
import { htmlEscape } from "./utils";
import { htmlEscape, trace } from "./utils";
const levelToFn = {
[LogLevel.DEBUG.toString()]: LogService.debug,
@@ -64,6 +64,7 @@ export default class ManagementRoomOutput {
* @param msgtype The desired message type of the returned TextualMessageEventContent
* @returns A TextualMessageEventContent with replaced room IDs
*/
@trace('ManagementRoomOutput.replaceRoomIdsWithPills')
private async replaceRoomIdsWithPills(text: string, roomIds: Set<string>, msgtype: MessageType = "m.text"): Promise<TextualMessageEventContent> {
const content: TextualMessageEventContent = {
body: text,
+15 -1
View File
@@ -34,7 +34,7 @@ import {
import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES } from "./models/ListRule";
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
import { htmlEscape } from "./utils";
import { htmlEscape, trace, traceSync } from "./utils";
import { ReportManager } from "./report/ReportManager";
import { ReportPoller } from "./report/ReportPoller";
import { WebAPIs } from "./webapis/WebAPIs";
@@ -108,6 +108,7 @@ export class Mjolnir {
* @param {boolean} options.autojoinOnlyIfManager Whether to only accept an invitation by a user present in the `managementRoom`.
* @param {string} options.acceptInvitesFromSpace A space of users to accept invites from, ignores invites form users not in this space.
*/
@trace('Mjolnir.addJoinOnInviteListener')
private static addJoinOnInviteListener(mjolnir: Mjolnir, client: MatrixSendClient, options: { [key: string]: any }) {
mjolnir.matrixEmitter.on("room.invite", async (roomId: string, inviteEvent: any) => {
const membershipEvent = new MembershipEvent(inviteEvent);
@@ -152,6 +153,7 @@ export class Mjolnir {
* @param {MatrixSendClient} client The client for Mjolnir to use.
* @returns A new Mjolnir instance that can be started without further setup.
*/
@trace('Mjolnir.setupMjolnirFromConfig')
static async setupMjolnirFromConfig(client: MatrixSendClient, matrixEmitter: MatrixEmitter, config: IConfig): Promise<Mjolnir> {
if (!config.autojoinOnlyIfManager && config.acceptInvitesFromSpace === getDefaultConfig().acceptInvitesFromSpace) {
throw new TypeError("`autojoinOnlyIfManager` has been disabled, yet no space has been provided for `acceptInvitesFromSpace`.");
@@ -286,6 +288,7 @@ export class Mjolnir {
/**
* Start Mjölnir.
*/
@trace('Mjolnir.start')
public async start() {
try {
// Start the web server.
@@ -351,6 +354,7 @@ export class Mjolnir {
/**
* Stop Mjolnir from syncing and processing commands.
*/
@traceSync('Mjolnir.stop')
public stop() {
LogService.info("Mjolnir", "Stopping Mjolnir...");
this.matrixEmitter.stop();
@@ -374,6 +378,7 @@ export class Mjolnir {
* use `protectRoom` instead.
* @param roomId The room to be explicitly protected by mjolnir and persisted in config.
*/
@trace('Mjolnir.addProtectedRoom')
public async addProtectedRoom(roomId: string) {
await this.protectedRoomsConfig.addProtectedRoom(roomId);
this.protectRoom(roomId);
@@ -383,6 +388,7 @@ export class Mjolnir {
* Protect the room, but do not persist it to the account data.
* @param roomId The room to protect.
*/
@traceSync('Mjolnir.protectRoom')
private protectRoom(roomId: string): void {
this.protectedRoomsTracker.addProtectedRoom(roomId);
this.roomJoins.addRoom(roomId);
@@ -394,6 +400,7 @@ export class Mjolnir {
* use `unprotectRoom` instead.
* @param roomId The room to remove from account data and stop protecting.
*/
@trace('Mjolnir.removeProtectedRoom')
public async removeProtectedRoom(roomId: string) {
await this.protectedRoomsConfig.removeProtectedRoom(roomId);
this.unprotectRoom(roomId);
@@ -403,6 +410,7 @@ export class Mjolnir {
* Unprotect a room.
* @param roomId The room to stop protecting.
*/
@trace('Mjolnir.unprotectRoom')
private unprotectRoom(roomId: string): void {
this.roomJoins.removeRoom(roomId);
this.protectedRoomsTracker.removeProtectedRoom(roomId);
@@ -413,6 +421,7 @@ export class Mjolnir {
* This is to implement `config.protectAllJoinedRooms` functionality.
* @param withSync Whether to synchronize all protected rooms with the watched policy lists afterwards.
*/
@trace('Mjolnir.resyncJoinedRooms')
private async resyncJoinedRooms(withSync = true): Promise<void> {
if (!this.config.protectAllJoinedRooms) return;
@@ -450,6 +459,7 @@ export class Mjolnir {
}
}
@trace('Mjolnir.handleEvent')
private async handleEvent(roomId: string, event: any) {
// Check for UISI errors
if (roomId === this.managementRoomId) {
@@ -477,6 +487,7 @@ export class Mjolnir {
}
}
@trace('Mjolnir.isSynapseAdmin')
public async isSynapseAdmin(): Promise<boolean> {
try {
const endpoint = `/_synapse/admin/v1/users/${await this.client.getUserId()}/admin`;
@@ -488,11 +499,13 @@ export class Mjolnir {
}
}
@trace('Mjolnir.deactivateSynapseUser')
public async deactivateSynapseUser(userId: string): Promise<any> {
const endpoint = `/_synapse/admin/v1/deactivate/${userId}`;
return await this.client.doRequest("POST", endpoint);
}
@trace('Mjolnir.shutdownSynapseRoom')
public async shutdownSynapseRoom(roomId: string, message?: string): Promise<any> {
const endpoint = `/_synapse/admin/v1/rooms/${roomId}`;
return await this.client.doRequest("DELETE", endpoint, null, {
@@ -507,6 +520,7 @@ export class Mjolnir {
* @param roomId the room where the user (or the bot) shall be made administrator.
* @param userId optionally specify the user mxID to be made administrator.
*/
@trace('Mjolnir.makeUserRoomAdmin')
public async makeUserRoomAdmin(roomId: string, userId: string): Promise<void> {
const endpoint = `/_synapse/admin/v1/rooms/${roomId}/make_room_admin`;
return await this.client.doRequest("POST", endpoint, null, {
+8 -1
View File
@@ -30,6 +30,7 @@ import { LogService } from "matrix-bot-sdk";
import { Permalinks } from './commands/interface-manager/Permalinks';
import { IConfig } from "./config";
import { MatrixSendClient } from './MatrixEmitter';
import { trace, traceSync } from './utils';
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
/**
@@ -54,6 +55,7 @@ export default class ProtectedRoomsConfig {
* Will also ensure we are able to join all of the rooms.
* @param config The config to load the rooms from under `config.protectedRooms`.
*/
@trace('ProtectedRoomsConfig.loadProtectedRoomsFromConfig')
public async loadProtectedRoomsFromConfig(config: IConfig): Promise<void> {
// Ensure we're also joined to the rooms we're protecting
LogService.info("ProtectedRoomsConfig", "Resolving protected rooms...");
@@ -74,6 +76,7 @@ export default class ProtectedRoomsConfig {
* Load any rooms that have been explicitly protected from the account data of the mjolnir user.
* Will not ensure we can join all the rooms. This so mjolnir can continue to operate if bogus rooms have been persisted to the account data.
*/
@trace('ProtectedRoomsConfig.loadProtectedRoomsFromAccountData')
public async loadProtectedRoomsFromAccountData(): Promise<void> {
LogService.debug("ProtectedRoomsConfig", "Loading protected rooms...");
try {
@@ -96,6 +99,7 @@ export default class ProtectedRoomsConfig {
* Save the room as explicitly protected.
* @param roomId The room to persist as explicitly protected.
*/
@trace('ProtectedRoomsConfig.addProtectedRoom')
public async addProtectedRoom(roomId: string): Promise<void> {
this.explicitlyProtectedRooms.add(roomId);
await this.saveProtectedRoomsToAccountData();
@@ -105,6 +109,7 @@ export default class ProtectedRoomsConfig {
* Remove the room from the explicitly protected set of rooms.
* @param roomId The room that should no longer be persisted as protected.
*/
@trace('ProtectedRoomsConfig.removeProtectedRoom')
public async removeProtectedRoom(roomId: string): Promise<void> {
this.explicitlyProtectedRooms.delete(roomId);
await this.saveProtectedRoomsToAccountData([roomId]);
@@ -115,6 +120,7 @@ export default class ProtectedRoomsConfig {
* This will NOT be the complete set of protected rooms, if `config.protectAllJoinedRooms` is true and should never be treated as the complete set.
* @returns The rooms that are marked as explicitly protected in both the config and Mjolnir's account data.
*/
@traceSync('ProtectedRoomsConfig.getExplicitlyProtectedRooms')
public getExplicitlyProtectedRooms(): string[] {
return [...this.explicitlyProtectedRooms.keys()]
}
@@ -123,13 +129,14 @@ export default class ProtectedRoomsConfig {
* Persist the set of explicitly protected rooms to the client's account data.
* @param excludeRooms Rooms that should not be persisted to the account data, and removed if already present.
*/
@trace('ProtectedRoomsConfig.saveProtectedRoomsToAccountData')
private async saveProtectedRoomsToAccountData(excludeRooms: string[] = []): Promise<void> {
// NOTE: this stops Mjolnir from racing with itself when saving the config
// but it doesn't stop a third party client on the same account racing with us instead.
await this.accountDataLock.acquireAsync();
try {
const additionalProtectedRooms: string[] = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE)
.then((rooms: {rooms?: string[]}) => Array.isArray(rooms?.rooms) ? rooms.rooms : [])
.then((rooms: { rooms?: string[] }) => Array.isArray(rooms?.rooms) ? rooms.rooms : [])
.catch(e => (LogService.warn("ProtectedRoomsConfig", "Could not load protected rooms from account data", e), []));
const roomsToSave = new Set([...this.explicitlyProtectedRooms.keys(), ...additionalProtectedRooms]);
+19 -2
View File
@@ -38,7 +38,7 @@ import { RoomUpdateError } from "./models/RoomUpdateError";
import { ProtectionManager } from "./protections/ProtectionManager";
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
import { htmlEscape } from "./utils";
import { htmlEscape, trace, traceSync } from "./utils";
/**
* This class aims to synchronize `m.ban` rules in a set of policy lists with
@@ -141,6 +141,7 @@ export class ProtectedRoomsSet {
* @param userId The user whose messages we want to redact.
* @param roomId The room we want to redact them in.
*/
@traceSync('ProtectedRoomsSet.redactUser')
public redactUser(userId: string, roomId: string) {
this.eventRedactionQueue.add(new RedactUserInRoom(userId, roomId));
}
@@ -155,14 +156,17 @@ export class ProtectedRoomsSet {
return this.automaticRedactionReasons;
}
public getProtectedRooms () {
@traceSync('ProtectedRoomsSet.getProtectedRooms')
public getProtectedRooms() {
return [...this.protectedRooms.keys()]
}
@traceSync('ProtectedRoomsSet.isProtectedRoom')
public isProtectedRoom(roomId: string): boolean {
return this.protectedRooms.has(roomId);
}
@traceSync('ProtectedRoomsSet.watchList')
public watchList(policyList: PolicyList): void {
if (!this.policyLists.includes(policyList)) {
this.policyLists.push(policyList);
@@ -171,6 +175,7 @@ export class ProtectedRoomsSet {
}
}
@traceSync('ProtectedRoomsSet.unwatchList')
public unwatchList(policyList: PolicyList): void {
this.policyLists = this.policyLists.filter(list => list.roomId !== policyList.roomId);
this.accessControlUnit.unwatchList(policyList);
@@ -185,6 +190,7 @@ export class ProtectedRoomsSet {
* @param roomId Limit processing to one room only, otherwise process redactions for all rooms.
* @returns The list of errors encountered, for reporting to the management room.
*/
@trace('ProtectedRoomsSet.processRedactionQueue')
public async processRedactionQueue(roomId?: string): Promise<RoomUpdateError[]> {
return await this.eventRedactionQueue.process(this.client, this.managementRoomOutput, roomId);
}
@@ -192,10 +198,12 @@ export class ProtectedRoomsSet {
/**
* @returns The protected rooms ordered by the most recently active first.
*/
@traceSync('ProtectedRoomsSet.protectedRoomsByActivity')
public protectedRoomsByActivity(): string[] {
return this.protectedRoomActivityTracker.protectedRoomsByActivity();
}
@trace('ProtectedRoomsSet.handleEvent')
public async handleEvent(roomId: string, event: any) {
if (event['sender'] === this.clientUserId) {
throw new TypeError("`ProtectedRooms::handleEvent` should not be used to inform about events sent by mjolnir.");
@@ -229,6 +237,7 @@ export class ProtectedRoomsSet {
/**
* Synchronize all the protected rooms with all of the policies described in the watched policy lists.
*/
@trace('ProtectedRoomsSet.syncRoomsWithPolicies')
private async syncRoomsWithPolicies() {
let hadErrors = false;
const [aclErrors, banErrors] = await Promise.all([
@@ -256,6 +265,7 @@ export class ProtectedRoomsSet {
* Update each watched list and then synchronize all the protected rooms with all the policies described in the watched lists,
* banning and applying any changed ACLS via `syncRoomsWithPolicies`.
*/
@trace('ProtectedRoomsSet.syncLists')
public async syncLists() {
for (const list of this.policyLists) {
const { revision } = await list.updateList();
@@ -268,6 +278,7 @@ export class ProtectedRoomsSet {
await this.syncRoomsWithPolicies();
}
@traceSync('ProtectedRoomsSet.addProtectedRoom')
public addProtectedRoom(roomId: string): void {
if (this.protectedRooms.has(roomId)) {
// we need to protect ourselves form syncing all the lists unnecessarily
@@ -278,6 +289,7 @@ export class ProtectedRoomsSet {
this.protectedRoomActivityTracker.addProtectedRoom(roomId);
}
@traceSync('ProtectedRoomsSet.removeProtectedRoom')
public removeProtectedRoom(roomId: string): void {
this.protectedRoomActivityTracker.removeProtectedRoom(roomId);
this.protectedRooms.delete(roomId);
@@ -290,6 +302,7 @@ export class ProtectedRoomsSet {
* @param policyList The `PolicyList` which we will check for changes and apply them to all protected rooms.
* @returns When all of the protected rooms have been updated.
*/
@trace('ProtectedRoomsSet.syncWithUpdatedPolicyList')
private async syncWithUpdatedPolicyList(policyList: PolicyList, changes: ListRuleChange[], revision: Revision): Promise<void> {
// avoid resyncing the rooms if we have already done so for the latest revision of this list.
const previousRevision = this.listRevisions.get(policyList);
@@ -310,6 +323,7 @@ export class ProtectedRoomsSet {
* @param {string[]} roomIds The room IDs to apply the ACLs in.
* @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with.
*/
@trace('ProtectedRoomsSet.applyServerAcls')
private async applyServerAcls(lists: PolicyList[], roomIds: string[]): Promise<RoomUpdateError[]> {
// we need to provide mutual exclusion so that we do not have requests updating the m.room.server_acl event
// finish out of order and therefore leave the room out of sync with the policy lists.
@@ -370,6 +384,7 @@ export class ProtectedRoomsSet {
* @param {string[]} roomIds The room IDs to apply the bans in.
* @param {Mjolnir} mjolnir The Mjolnir client to apply the bans with.
*/
@trace('ProtectedRoomsSet.applyUserBans')
private async applyUserBans(roomIds: string[]): Promise<RoomUpdateError[]> {
// We can only ban people who are not already banned, and who match the rules.
const errors: RoomUpdateError[] = [];
@@ -441,6 +456,7 @@ export class ProtectedRoomsSet {
* @param changes A list of changes that have been made to a particular ban list.
* @returns true if the message was sent, false if it wasn't (because there there were no changes to report).
*/
@trace('ProtectedRoomsSet.printBanlistChanges')
private async printBanlistChanges(changes: ListRuleChange[], list: PolicyList): Promise<boolean> {
if (changes.length <= 0) return false;
@@ -519,6 +535,7 @@ export class ProtectedRoomsSet {
throw new TypeError("Unimplemented, need to put protections into here too.")
}
@trace('ProtectedRoomsSet.verifyPermissions')
public async verifyPermissions(verbose = true, printRegardless = false) {
const errors: RoomUpdateError[] = [];
for (const roomId of this.protectedRooms) {
+14
View File
@@ -1,4 +1,5 @@
import { MatrixEmitter } from "./MatrixEmitter";
import { trace, traceSync } from "./utils";
enum Action {
Join,
@@ -53,6 +54,7 @@ class RoomMembers {
/**
* Record a join.
*/
@traceSync("RoomMembers.join")
public join(userId: string, timestamp: number) {
this._joinsByTimestamp.push(new Join(userId, timestamp));
this._joinsByUser.set(userId, timestamp);
@@ -61,6 +63,7 @@ class RoomMembers {
/**
* Record a leave.
*/
@traceSync("RoomMembers.join")
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.
@@ -73,6 +76,7 @@ class RoomMembers {
/**
* Run a cleanup on the data structure.
*/
@traceSync("RoomMembers.cleanup")
public cleanup() {
if (this._leaves.size === 0) {
// Nothing to do.
@@ -87,6 +91,7 @@ class RoomMembers {
*
* @returns true if the `join` is still valid.
*/
@traceSync("RoomMembers.isStillValid")
private isStillValid(join: Join): boolean {
const leaveTS = this._leaves.get(join.userId);
if (!leaveTS) {
@@ -110,6 +115,7 @@ class RoomMembers {
* @returns A list of up to `max` members joined since `since`, ranked
* from most recent join to oldest join.
*/
@traceSync("RoomMembers.members")
public members(since: Date, max: number): Join[] {
const result = [];
const ts = since.getTime();
@@ -142,6 +148,7 @@ class RoomMembers {
* @returns a `Date` if the user is currently in the room and has joined
* since the start of Mjölnir, `null` otherwise.
*/
@traceSync("RoomMembers.get")
public get(userId: string): Date | null {
let ts = this._joinsByUser.get(userId);
if (!ts) {
@@ -163,6 +170,7 @@ export class RoomMemberManager {
/**
* Start listening to join/leave events in a room.
*/
@traceSync('RoomMemberManager.addRoom')
public addRoom(roomId: string) {
if (this.perRoom.has(roomId)) {
// Nothing to do.
@@ -176,10 +184,12 @@ export class RoomMemberManager {
*
* Cleanup any remaining data on join/leave events.
*/
@traceSync('RoomMemberManager.removeRoom')
public removeRoom(roomId: string) {
this.perRoom.delete(roomId);
}
@traceSync('RoomMemberManager.cleanup')
public cleanup(roomId: string) {
this.perRoom.get(roomId)?.cleanup();
}
@@ -187,6 +197,7 @@ export class RoomMemberManager {
/**
* Dispose of this object.
*/
@traceSync('RoomMemberManager.dispose')
public dispose() {
this.client.off("room.event", this.cbHandleEvent);
}
@@ -201,6 +212,7 @@ export class RoomMemberManager {
* `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.
*/
@traceSync('RoomMemberManager.getUserJoin')
public getUserJoin(user: { roomId: string, userId: string }): Date | null {
const { roomId, userId } = user;
const ts = this.perRoom.get(roomId)?.get(userId) || null;
@@ -215,6 +227,7 @@ export class RoomMemberManager {
*
* Only the users who have joined since the start of Mjölnir are returned.
*/
@traceSync('RoomMemberManager.getUsersInRoom')
public getUsersInRoom(roomId: string, since: Date, max = 100): Join[] {
const inRoom = this.perRoom.get(roomId);
if (!inRoom) {
@@ -226,6 +239,7 @@ export class RoomMemberManager {
/**
* Record join/leave events.
*/
@trace('RoomMemberManager.handleEvent')
public async handleEvent(roomId: string, event: any, now?: Date) {
if (event['type'] !== 'm.room.member') {
// Not a join/leave event.
+7 -1
View File
@@ -30,6 +30,7 @@ import { Permalinks } from "../commands/interface-manager/Permalinks";
import AccessControlUnit, { EntityAccess } from "../models/AccessControlUnit";
import { EntityType, Recommendation } from "../models/ListRule";
import PolicyList from "../models/PolicyList";
import { trace, traceSync } from "../utils";
/**
* Utility to manage which users have access to the application service,
@@ -41,7 +42,7 @@ export class AccessControl {
private constructor(
private readonly accessControlList: PolicyList,
private readonly accessControlUnit: AccessControlUnit
) {
) {
}
/**
@@ -50,6 +51,7 @@ export class AccessControl {
* @param bridge The matrix-appservice-bridge, used to get the appservice bot.
* @returns A new instance of `AccessControl` to be used by `MjolnirAppService`.
*/
@trace('AccessControl.setupAccessControl')
public static async setupAccessControl(
/** The room id for the access control list. */
accessControlListId: string,
@@ -66,20 +68,24 @@ export class AccessControl {
return new AccessControl(accessControlList, accessControlUnit);
}
@traceSync('AccessControl.handleEvent')
public handleEvent(roomId: string, event: any) {
if (roomId === this.accessControlList.roomId) {
this.accessControlList.updateForEvent(event);
}
}
@traceSync('AccessControl.getUserAccess')
public getUserAccess(mxid: string): EntityAccess {
return this.accessControlUnit.getAccessForUser(mxid, "CHECK_SERVER");
}
@trace('AccessControl.allow')
public async allow(mxid: string): Promise<void> {
await this.accessControlList.createPolicy(EntityType.RULE_USER, Recommendation.Allow, mxid);
}
@trace('AccessControl.remove')
public async remove(mxid: string): Promise<void> {
await this.accessControlList.unbanEntity(EntityType.RULE_USER, mxid);
}
+11 -3
View File
@@ -1,3 +1,4 @@
import { trace, traceSync } from '../utils';
import request from "request";
import express from "express";
import * as bodyParser from "body-parser";
@@ -16,14 +17,15 @@ export class Api {
constructor(
private homeserver: string,
private mjolnirManager: MjolnirManager,
) {}
) { }
/**
* Resolves an open id access token to find a matching user that the token is valid for.
* @param accessToken An openID token.
* @returns The mxid of the user that this token belongs to or null if the token could not be authenticated.
*/
private resolveAccessToken(accessToken: string): Promise<string|null> {
@traceSync('Api.resolveAccessToken')
private resolveAccessToken(accessToken: string): Promise<string | null> {
return new Promise((resolve, reject) => {
request({
url: `${this.homeserver}/_matrix/federation/v1/openid/userinfo`,
@@ -34,7 +36,7 @@ export class Api {
reject(null);
}
let response: { sub: string};
let response: { sub: string };
try {
response = JSON.parse(body);
} catch (e) {
@@ -48,6 +50,7 @@ export class Api {
});
}
@trace('Api.close')
public async close(): Promise<void> {
await new Promise((resolve, reject) => {
if (!this.httpServer) {
@@ -57,6 +60,7 @@ export class Api {
});
}
@traceSync('Api.start')
public start(port: number) {
if (this.httpServer) {
throw new TypeError("server already started");
@@ -76,6 +80,7 @@ export class Api {
* @param req.body.openId An OpenID token to verify that the sender of the request owns the mjolnir described in `req.body.mxid`.
* @param req.body.mxid The mxid of the mjolnir we want to find the management room for.
*/
@trace('Api.pathGet')
private async pathGet(req: express.Request, response: express.Response) {
const accessToken = req.body["openId"];
if (accessToken === undefined) {
@@ -110,6 +115,7 @@ export class Api {
* Return the mxids of mjolnirs that this user has provisioned.
* @param req.body.openId An OpenID token to find the sender of the request with and find their provisioned mjolnirs.
*/
@trace('Api.pathList')
private async pathList(req: express.Request, response: express.Response) {
const accessToken = req.body["openId"];
if (accessToken === undefined) {
@@ -133,6 +139,7 @@ export class Api {
* This is so that mjolnir can protect the room once the authenticity of the request has been verified.
* @param req.body.openId An OpenID token to find the sender of the request with.
*/
@trace('Api.pathCreate')
private async pathCreate(req: express.Request, response: express.Response) {
const accessToken = req.body["openId"];
if (accessToken === undefined) {
@@ -165,6 +172,7 @@ export class Api {
* @param req.body.mxid The mxid of the mjolnir that should join the room.
* @param req.body.roomId The room that this mjolnir should join and protect.
*/
@trace('Api.pathJoin')
private async pathJoin(req: express.Request, response: express.Response) {
const accessToken = req.body["openId"];
if (accessToken === undefined) {
+10
View File
@@ -34,8 +34,10 @@ import { IConfig } from "./config/config";
import { AccessControl } from "./AccessControl";
import { AppserviceCommandHandler } from "./bot/AppserviceCommandHandler";
import { SOFTWARE_VERSION } from "../config";
import { independentTrace, trace, traceSync } from "../utils";
const log = new Logger("AppService");
/**
* Responsible for setting up listeners and delegating functionality to a matrix-appservice-bridge `Bridge` for
* the entrypoint of the application.
@@ -68,6 +70,7 @@ export class MjolnirAppService {
* @param registrationFilePath A file path to the registration file to read the namespace and tokens from.
* @returns A new `MjolnirAppService`.
*/
@independentTrace('MjolnirAppService.run')
public static async makeMjolnirAppService(config: IConfig, dataStore: DataStore, registrationFilePath: string) {
const bridge = new Bridge({
homeserverUrl: config.homeserver.url,
@@ -115,6 +118,7 @@ export class MjolnirAppService {
* @param config The parsed configuration file.
* @param registrationFilePath A path to their homeserver registration file.
*/
@independentTrace('MjolnirAppService.run')
public static async run(port: number, config: IConfig, registrationFilePath: string): Promise<MjolnirAppService> {
Logger.configure(config.logging ?? { console: "debug" });
const dataStore = new PgDataStore(config.db.connectionString);
@@ -122,9 +126,11 @@ export class MjolnirAppService {
const service = await MjolnirAppService.makeMjolnirAppService(config, dataStore, registrationFilePath);
// The call to `start` MUST happen last. As it needs the datastore, and the mjolnir manager to be initialized before it can process events from the homeserver.
await service.start(port);
return service;
}
@traceSync('MjolnirAppService.onUserQuery')
public onUserQuery(queriedUser: MatrixUser) {
return {}; // auto-provision users with no additonal data
}
@@ -136,6 +142,7 @@ export class MjolnirAppService {
* @param request A matrix-appservice-bridge request encapsulating a Matrix event.
* @param context Additional context for the Matrix event.
*/
@trace('MjolnirAppService.onEvent')
public async onEvent(request: Request<WeakEvent>, context: BridgeContext) {
const mxEvent = request.getData();
// Provision a new mjolnir for the invitee when the appservice bot (designated by this.bridge.botUserId) is invited to a room.
@@ -166,6 +173,7 @@ export class MjolnirAppService {
* Start the appservice. See `run`.
* @param port The port that the appservice should listen on to receive transactions from the homeserver.
*/
@independentTrace('MjolnirAppService.run')
private async start(port: number) {
await this.bridge.getBot().getClient().joinRoom(this.config.adminRoom);
log.info("Starting MjolnirAppService, Matrix-side to listen on port", port);
@@ -186,6 +194,7 @@ export class MjolnirAppService {
/**
* Stop listening to requests from both the homeserver and web api and disconnect from the datastore.
*/
@trace('MjolnirAppService.close')
public async close(): Promise<void> {
await this.bridge.close();
await this.dataStore.close();
@@ -198,6 +207,7 @@ export class MjolnirAppService {
* @param reg Any existing parameters to be included in the registration, to be mutated by this method.
* @param callback To call when the registration has been generated with the final registration.
*/
@traceSync('MjolnirAppService.close')
public static generateRegistration(reg: AppServiceRegistration, callback: (finalRegistration: AppServiceRegistration) => void) {
reg.setId(AppServiceRegistration.generateToken());
reg.setHomeserverToken(AppServiceRegistration.generateToken());
+23
View File
@@ -12,6 +12,7 @@ import { MatrixEmitter } from "../MatrixEmitter";
import { Permalinks } from "../commands/interface-manager/Permalinks";
import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference";
import { Gauge } from "prom-client";
import { trace, traceSync } from "../utils";
const log = new Logger('MjolnirManager');
@@ -44,6 +45,7 @@ export class MjolnirManager {
* @param accessControl Who has access to the bridge.
* @returns A new mjolnir manager.
*/
@trace('MjolnirManager.makeMjolnirManager')
public static async makeMjolnirManager(dataStore: DataStore, bridge: Bridge, accessControl: AccessControl, instanceCountGauge: Gauge<"status" | "uuid">): Promise<MjolnirManager> {
const mjolnirManager = new MjolnirManager(dataStore, bridge, accessControl, instanceCountGauge);
await mjolnirManager.startMjolnirs(await dataStore.list());
@@ -57,6 +59,7 @@ export class MjolnirManager {
* @param client A client for the appservice virtual user that the new mjolnir should use.
* @returns A new managed mjolnir.
*/
@trace('MjolnirManager.makeInstance')
public async makeInstance(localPart: string, requestingUserId: string, managementRoomId: string, client: MatrixClient): Promise<ManagedMjolnir> {
const mxid = await client.getUserId();
const intentListener = new MatrixIntentListener(mxid);
@@ -93,6 +96,7 @@ export class MjolnirManager {
* @param ownerId The owner of the mjolnir. We ask for it explicitly to not leak access to another user's mjolnir.
* @returns The matching managed mjolnir instance.
*/
@traceSync('MjolnirManager.getMjolnir')
public getMjolnir(mjolnirId: string, ownerId: string): ManagedMjolnir | undefined {
const mjolnir = this.mjolnirs.get(mjolnirId);
if (mjolnir) {
@@ -111,6 +115,7 @@ export class MjolnirManager {
* @param ownerId An owner of multiple mjolnirs.
* @returns Any mjolnirs that they own.
*/
@traceSync('MjolnirManager.getOwnedMjolnirs')
public getOwnedMjolnirs(ownerId: string): ManagedMjolnir[] {
// TODO we need to use the database for this but also provide the utility
// for going from a MjolnirRecord to a ManagedMjolnir.
@@ -121,6 +126,7 @@ export class MjolnirManager {
/**
* Listener that should be setup and called by `MjolnirAppService` while listening to the bridge abstraction provided by matrix-appservice-bridge.
*/
@traceSync('MjolnirManager.onEvent')
public onEvent(request: Request<WeakEvent>, context: BridgeContext) {
// TODO We need a way to map a room id (that the event is from) to a set of managed mjolnirs that should be informed.
// https://github.com/matrix-org/mjolnir/issues/412
@@ -132,6 +138,7 @@ export class MjolnirManager {
* @param requestingUserId The mxid of the user we are creating a mjolnir for.
* @returns The matrix id of the new mjolnir and its management room.
*/
@trace('MjolnirManager.provisionNewMjolnir')
public async provisionNewMjolnir(requestingUserId: string): Promise<[string, string]> {
const access = this.accessControl.getUserAccess(requestingUserId);
if (access.outcome !== Access.Allowed) {
@@ -170,14 +177,17 @@ export class MjolnirManager {
}
}
@traceSync('MjolnirManager.reportUnstartedMjolnir')
public reportUnstartedMjolnir(code: UnstartedMjolnir.FailCode, cause: any, mjolnirRecord: MjolnirRecord, mxid: string): void {
this.unstartedMjolnirs.set(mjolnirRecord.local_part, new UnstartedMjolnir(mjolnirRecord, new UserID(mxid), code, cause));
}
@traceSync('MjolnirManager.getUnstartedMjolnirs')
public getUnstartedMjolnirs(): UnstartedMjolnir[] {
return [...this.unstartedMjolnirs.values()];
}
@traceSync('MjolnirManager.findUnstartedMjolnir')
public findUnstartedMjolnir(localPart: string): UnstartedMjolnir | undefined {
return [...this.unstartedMjolnirs.values()].find(unstarted => unstarted.mjolnirRecord.local_part === localPart);
}
@@ -187,6 +197,7 @@ export class MjolnirManager {
* @param localPart The localpart of the virtual user we need a client for.
* @returns A bridge intent with the complete mxid of the virtual user and a MatrixClient.
*/
@trace('MjolnirManager.makeMatrixIntent')
private async makeMatrixIntent(localPart: string): Promise<Intent> {
const mjIntent = this.bridge.getIntentFromLocalpart(localPart);
await mjIntent.ensureRegistered();
@@ -198,6 +209,7 @@ export class MjolnirManager {
* Will be added to `this.unstartedMjolnirs` if we fail to start it AND it is not already running.
* @param mjolnirRecord The record for the mjolnir that we want to start.
*/
@trace('MjolnirManager.startMjolnir')
public async startMjolnir(mjolnirRecord: MjolnirRecord): Promise<void> {
// if a mjolnir is in `this.mjonirs` it is started, as if it is present, it is going to be given Matrix events.
if (this.mjolnirs.has(mjolnirRecord.local_part)) {
@@ -244,6 +256,7 @@ export class MjolnirManager {
/**
* Used at startup to create all the ManagedMjolnir instances and start them so that they will respond to users.
*/
@trace('MjolnirManager.startMjolnirs')
public async startMjolnirs(mjolnirRecords: MjolnirRecord[]): Promise<void> {
for (const mjolnirRecord of mjolnirRecords) {
await this.startMjolnir(mjolnirRecord);
@@ -258,17 +271,21 @@ export class ManagedMjolnir {
private readonly matrixEmitter: MatrixIntentListener,
) { }
@trace('ManagedMjolnir.onEvent')
public async onEvent(request: Request<WeakEvent>) {
this.matrixEmitter.handleEvent(request.getData());
}
@trace('ManagedMjolnir.joinRoom')
public async joinRoom(roomId: string) {
await this.mjolnir.client.joinRoom(roomId);
}
@trace('ManagedMjolnir.addProtectedRoom')
public async addProtectedRoom(roomId: string) {
await this.mjolnir.addProtectedRoom(roomId);
}
@trace('ManagedMjolnir.createFirstList')
public async createFirstList(mjolnirOwnerId: string, shortcode: string) {
const listRoomId = await PolicyList.createList(
this.mjolnir.client,
@@ -281,6 +298,8 @@ export class ManagedMjolnir {
return await this.mjolnir.policyListManager.watchList(roomRef);
}
// @ts-ignore (Due to annotation)
@traceSync('ManagedMjolnir.managementRoomId')
public get managementRoomId(): string {
return this.mjolnir.managementRoomId;
}
@@ -289,6 +308,7 @@ export class ManagedMjolnir {
* Intended to be called by the MjolnirManager to make sure the mjolnir is ready to listen to events.
* This managed mjolnir should not be informed of any events via `onEvent` until `start` is called.
*/
@trace('ManagedMjolnir.start')
public async start(): Promise<void> {
await this.mjolnir.start();
}
@@ -306,6 +326,7 @@ export class MatrixIntentListener extends EventEmitter implements MatrixEmitter
super()
}
@traceSync('MatrixIntentListener.handleEvent')
public handleEvent(mxEvent: WeakEvent) {
// These are ordered to be the same as matrix-bot-sdk's MatrixClient
// They shouldn't need to be, but they are just in case it matters.
@@ -333,6 +354,7 @@ export class MatrixIntentListener extends EventEmitter implements MatrixEmitter
/**
* To be called by `Mjolnir`.
*/
@trace('MatrixIntentListener.start')
public async start() {
// Nothing to do.
}
@@ -340,6 +362,7 @@ export class MatrixIntentListener extends EventEmitter implements MatrixEmitter
/**
* To be called by `Mjolnir`.
*/
@traceSync('MatrixIntentListener.stop')
public stop() {
// Nothing to do.
}
@@ -24,6 +24,7 @@ import '../../commands/interface-manager/MatrixPresentations';
import './ListCommand';
import './AccessCommands';
import { AppserviceBotEmitter } from './AppserviceBotEmitter';
import { traceSync } from '../../utils';
defineInterfaceCommand({
@@ -48,6 +49,7 @@ export class AppserviceCommandHandler {
}
@traceSync('AppserviceCommandHandler.handleEvent')
public handleEvent(mxEvent: WeakEvent): void {
if (mxEvent.type !== 'm.room.message' && mxEvent.room_id !== this.appservice.config.adminRoom) {
return;
+69 -1
View File
@@ -1,3 +1,70 @@
// Order of import matters
import { NodeSDK } from "@opentelemetry/sdk-node";
import { AlwaysOnSampler, Sampler, SamplingDecision } from '@opentelemetry/sdk-trace-base';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { DiagConsoleLogger, DiagLogLevel, Attributes, SpanKind, diag } from '@opentelemetry/api';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
/**
* This starts instrumentation for the app
*/
if (process.env.TRACING_ENABLED) {
if (process.env.TRACING_DIAG_ENABLED) {
if (process.env.TRACING_DIAG_DEBUG) {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
} else if (process.env.TRACING_DIAG_VERBOSE) {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.VERBOSE);
} else {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
}
}
console.info("Preparing tracing");
type FilterFunction = (spanName: string, spanKind: SpanKind, attributes: Attributes) => boolean;
function filterSampler(filterFn: FilterFunction, parent: Sampler): Sampler {
return {
shouldSample(ctx, tid, spanName, spanKind, attr, links) {
if (!filterFn(spanName, spanKind, attr)) {
return { decision: SamplingDecision.NOT_RECORD };
}
return parent.shouldSample(ctx, tid, spanName, spanKind, attr, links);
},
toString() {
return `FilterSampler(${parent.toString()})`;
}
}
}
function ignoreHealthCheck(spanName: string, spanKind: SpanKind, attributes: Attributes) {
return spanKind !== SpanKind.SERVER || attributes[SemanticAttributes.HTTP_ROUTE] !== "/healthz" || attributes[SemanticAttributes.HTTP_ROUTE] !== "/metrics";
}
if (process.env.TRACING_TRACE_URL === undefined || process.env.TRACING_TRACE_URL === "") {
console.error("Unable to start tracing without the env variable `TRACING_TRACE_URL` being set. Check https://opentelemetry.io/docs/instrumentation/js/exporters/ for more infomration.");
process.exit(1);
}
console.info(`Starting tracing and pushing to ${process.env.TRACING_TRACE_URL}`);
const exporter = new OTLPTraceExporter({
//url: "<your-otlp-endpoint>/v1/traces",
url: process.env.TRACING_TRACE_URL
});
const sdk = new NodeSDK({
sampler: filterSampler(ignoreHealthCheck, new AlwaysOnSampler()),
traceExporter: exporter,
serviceName: "Draupnir-Appservice",
instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start();
console.info("Started tracing");
} else {
console.warn("Running without tracing");
}
import { Cli } from "matrix-appservice-bridge";
import { MjolnirAppService } from "./AppService";
import { IConfig } from "./config/config";
@@ -15,7 +82,7 @@ const cli = new Cli({
defaults: {}
},
generateRegistration: MjolnirAppService.generateRegistration,
run: async function(port: number) {
run: async function (port: number) {
const config: IConfig | null = cli.getConfig() as any;
if (config === null) {
throw new Error("Couldn't load config");
@@ -23,5 +90,6 @@ const cli = new Cli({
await MjolnirAppService.run(port, config, cli.getRegistrationFilePath());
}
});
console.log("Starting to run appservice");
cli.run();
+7
View File
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { trace } from '../../utils';
import { PostgresStore, SchemaUpdateFunction } from "matrix-appservice-bridge";
import { DataStore, MjolnirRecord } from "../datastore";
@@ -32,14 +33,17 @@ export class PgDataStore extends PostgresStore implements DataStore {
super(getSchema(), { url: connectionString })
}
@trace('PgDataStore.init')
public async init(): Promise<void> {
await this.ensureSchema();
}
@trace('PgDataStore.close')
public async close(): Promise<void> {
await this.destroy();
}
@trace('PgDataStore.list')
public async list(): Promise<MjolnirRecord[]> {
const result = await this.sql`SELECT local_part, owner, management_room FROM mjolnir`;
if (!result.count) {
@@ -49,17 +53,20 @@ export class PgDataStore extends PostgresStore implements DataStore {
return result.flat() as MjolnirRecord[];
}
@trace('PgDataStore.store')
public async store(mjolnirRecord: MjolnirRecord): Promise<void> {
await this.sql`INSERT INTO mjolnir (local_part, owner, management_room)
VALUES (${mjolnirRecord.local_part}, ${mjolnirRecord.owner}, ${mjolnirRecord.management_room})`;
}
@trace('PgDataStore.lookupByOwner')
public async lookupByOwner(owner: string): Promise<MjolnirRecord[]> {
const result = await this.sql`SELECT local_part, owner, management_room FROM mjolnir
WHERE owner = ${owner}`;
return result.flat() as MjolnirRecord[];
}
@trace('PgDataStore.lookupByLocalPart')
public async lookupByLocalPart(localPart: string): Promise<MjolnirRecord[]> {
const result = await this.sql`SELECT local_part, owner, management_room FROM mjolnir
WHERE local_part = ${localPart}`;
@@ -5,6 +5,7 @@
import { randomUUID } from "crypto";
import { CommandError, CommandResult } from "./Validation";
import { traceSync } from "../../utils";
// FIXME: I wonder if we could allow message to be JSX?
// Then room references could be put into the DM and actually mean something.
@@ -12,11 +13,12 @@ export class CommandException extends CommandError {
public readonly uuid = randomUUID();
constructor(
public readonly exception: Error|unknown,
public readonly exception: Error | unknown,
message: string) {
super(message)
}
@traceSync('CommandException.Result')
public static Result<Ok>(message: string, options: { exception: Error }): CommandResult<Ok, CommandException> {
return CommandResult.Err(new CommandException(options.exception, message));
}
@@ -6,16 +6,17 @@
import { UserID } from "matrix-bot-sdk";
import { MatrixRoomReference } from "./MatrixRoomReference";
import { Permalinks } from "./Permalinks";
import { traceSync } from "../../utils";
export interface ISuperCoolStream<T> {
readonly source: T
peekItem(eof?: any): T|any,
readItem(eof?: any): T|any,
peekItem(eof?: any): T | any,
readItem(eof?: any): T | any,
getPosition(): number,
savingPositionIf<Result>(description: { predicate: (t: Result) => boolean, body: (stream: ISuperCoolStream<T>) => Result}): Result;
savingPositionIf<Result>(description: { predicate: (t: Result) => boolean, body: (stream: ISuperCoolStream<T>) => Result }): Result;
}
export class SuperCoolStream<T extends { at: (...args: any) => any|undefined}> implements ISuperCoolStream<T> {
export class SuperCoolStream<T extends { at: (...args: any) => any | undefined }> implements ISuperCoolStream<T> {
protected position: number
/**
* Makes the super cool string stream.
@@ -26,18 +27,22 @@ export class SuperCoolStream<T extends { at: (...args: any) => any|undefined}> i
this.position = start;
}
@traceSync('SuperCoolStream.pop')
public peekItem(eof = undefined) {
return this.source.at(this.position) ?? eof;
}
@traceSync('SuperCoolStream.readItem')
public readItem(eof = undefined) {
return this.source.at(this.position++) ?? eof;
}
@traceSync('SuperCoolStream.getPosition')
public getPosition(): number {
return this.position;
}
@traceSync('SuperCoolStream.savingPositionIf')
savingPositionIf<Result>(description: { predicate: (t: Result) => boolean; body: (stream: SuperCoolStream<T>) => Result; }): Result {
const previousPosition = this.position;
const bodyResult = description.body(this);
@@ -52,10 +57,12 @@ export class SuperCoolStream<T extends { at: (...args: any) => any|undefined}> i
* Helper for peeking and reading character by character.
*/
class StringStream extends SuperCoolStream<string> {
@traceSync('StringStream.peekChar')
public peekChar(...args: any[]) {
return this.peekItem(...args);
}
@traceSync('StringStream.readChar')
public readChar(...args: any[]) {
return this.readItem(...args);
}
@@ -153,7 +160,7 @@ function defineReadItem(dispatchCharacter: string, macro: ReadMacro) {
WORD_DISPATCH_CHARACTERS.set(dispatchCharacter, macro);
}
type PostReadStringReplaceTransformer = (item: string) => ReadItem|string;
type PostReadStringReplaceTransformer = (item: string) => ReadItem | string;
type TransformerEntry = { regex: RegExp, transformer: PostReadStringReplaceTransformer };
const POST_READ_TRANSFORMERS = new Map<string, TransformerEntry>();
@@ -204,7 +211,7 @@ function readUntil(regex: RegExp, stream: StringStream, output: string[]) {
* @param stream The stream to consume the room reference from.
* @returns A MatrixRoomReference or string if what has been read does not represent a room.
*/
function readRoomIDOrAlias(stream: StringStream): MatrixRoomReference|string {
function readRoomIDOrAlias(stream: StringStream): MatrixRoomReference | string {
const word: string[] = [stream.readChar()!];
readUntil(/[:\s]/, stream, word);
if (stream.peekChar() === undefined || WHITESPACE.includes(stream.peekChar()!)) {
@@ -223,7 +230,7 @@ defineReadItem('!', readRoomIDOrAlias);
/**
* Read the word as a UserID, otherwise return a string if what has been read doesn not represent a user.
*/
defineReadItem('@', (stream: StringStream): UserID|string => {
defineReadItem('@', (stream: StringStream): UserID | string => {
const word: string[] = [stream.readChar()!];
readUntil(/[:\s]/, stream, word);
if (stream.peekChar() === undefined || WHITESPACE.includes(stream.peekChar()!)) {
+29 -16
View File
@@ -3,6 +3,7 @@
* All rights reserved.
*/
import { traceSync } from "../../utils";
import { SuperCoolStream } from "./CommandReader";
/**
@@ -27,7 +28,7 @@ import { SuperCoolStream } from "./CommandReader";
*/
export interface AbstractNode {
readonly parent: DocumentNode|null;
readonly parent: DocumentNode | null;
readonly leafNode: boolean;
readonly tag: NodeTag;
}
@@ -35,9 +36,9 @@ export interface AbstractNode {
export interface DocumentNode extends AbstractNode {
readonly leafNode: false;
attributeMap: Map<string, any>,
addChild<Node extends DocumentNode|LeafNode>(node: Node): Node
getChildren(): (DocumentNode|LeafNode)[]
getFirstChild(): DocumentNode|LeafNode|undefined;
addChild<Node extends DocumentNode | LeafNode>(node: Node): Node
getChildren(): (DocumentNode | LeafNode)[]
getFirstChild(): DocumentNode | LeafNode | undefined;
}
export interface LeafNode extends AbstractNode {
@@ -74,11 +75,11 @@ export enum NodeTag {
* where we can use ad-hoc mixins.
*/
interface DeadDocumentNode extends DocumentNode {
children: (DocumentNode|LeafNode)[];
children: (DocumentNode | LeafNode)[];
attributeMap: Map<string, any>,
}
export function addChild<Node extends DocumentNode|LeafNode>(this: DeadDocumentNode, node: Node): Node {
export function addChild<Node extends DocumentNode | LeafNode>(this: DeadDocumentNode, node: Node): Node {
if (this.children.includes(node)) {
return node;
}
@@ -86,11 +87,11 @@ export function addChild<Node extends DocumentNode|LeafNode>(this: DeadDocumentN
return node;
}
export function getChildren(this: DeadDocumentNode): (DocumentNode|LeafNode)[] {
export function getChildren(this: DeadDocumentNode): (DocumentNode | LeafNode)[] {
return [...this.children];
}
export function getFirstChild(this: DeadDocumentNode): DocumentNode|LeafNode|undefined {
export function getFirstChild(this: DeadDocumentNode): DocumentNode | LeafNode | undefined {
return this.children.at(0);
}
@@ -153,7 +154,7 @@ export enum FringeType {
type AnnotatedFringeNode = {
type: FringeType,
node: DocumentNode|LeafNode
node: DocumentNode | LeafNode
};
type Flat = AnnotatedFringeNode[];
@@ -166,13 +167,13 @@ function fringeInternalNode(node: DocumentNode, flat: Flat): Flat {
if (node.getChildren().length === 0) {
return flat;
} else {
return node.getChildren().reduce((previous: Flat, child: DocumentNode|LeafNode) => {
return node.getChildren().reduce((previous: Flat, child: DocumentNode | LeafNode) => {
return fringe(child, previous);
}, flat);
}
}
function fringe(node: DocumentNode|LeafNode, flat: Flat = []): Flat {
function fringe(node: DocumentNode | LeafNode, flat: Flat = []): Flat {
if (node.leafNode) {
flat.push({ type: FringeType.Leaf, node });
return flat;
@@ -200,6 +201,7 @@ export class SimpleFringeRenderer<Context> implements FringeRenderer<Context> {
}
@traceSync('SimpleFringeRenderer.getRenderer')
private getRenderer<T>(table: Map<NodeTag, T>, type: FringeType, tag: NodeTag): T {
const entry = table.get(tag);
if (entry) {
@@ -208,26 +210,31 @@ export class SimpleFringeRenderer<Context> implements FringeRenderer<Context> {
throw new TypeError(`Couldn't find a ${type} renderer for ${tag}`);
}
@traceSync('SimpleFringeRenderer.getPreRenderer')
public getPreRenderer(tag: NodeTag): FringeInnerRenderFunction<Context> {
return this.getRenderer(this.preRenderers, FringeType.Pre, tag);
}
@traceSync('SimpleFringeRenderer.getLeafRenderer')
public getLeafRenderer(tag: NodeTag): FringeLeafRenderFunction<Context> {
return this.getRenderer(this.leafRenderers, FringeType.Leaf, tag);
}
@traceSync('SimpleFringeRenderer.getPostRenderer')
public getPostRenderer(tag: NodeTag): FringeInnerRenderFunction<Context> {
return this.getRenderer(this.postRenderers, FringeType.Post, tag);
}
public internRenderer<T extends FringeInnerRenderFunction<Context>|FringeLeafRenderFunction<Context>>(type: FringeType, tag: NodeTag, table: Map<NodeTag, T>, renderer: T): void {
@traceSync('SimpleFringeRenderer.internRenderer')
public internRenderer<T extends FringeInnerRenderFunction<Context> | FringeLeafRenderFunction<Context>>(type: FringeType, tag: NodeTag, table: Map<NodeTag, T>, renderer: T): void {
if (table.has(tag)) {
throw new TypeError(`There is already a renderer registered for ${type} ${tag}`);
}
table.set(tag, renderer);
}
public registerRenderer<T extends FringeInnerRenderFunction<Context>|FringeLeafRenderFunction<Context>>(type: FringeType, tag: NodeTag, renderer: T): SimpleFringeRenderer<Context> {
@traceSync('SimpleFringeRenderer.registerRenderer')
public registerRenderer<T extends FringeInnerRenderFunction<Context> | FringeLeafRenderFunction<Context>>(type: FringeType, tag: NodeTag, renderer: T): SimpleFringeRenderer<Context> {
// The casting in here is evil. Not sure how to fix it.
switch (type) {
case FringeType.Pre:
@@ -243,6 +250,7 @@ export class SimpleFringeRenderer<Context> implements FringeRenderer<Context> {
return this;
}
@traceSync('SimpleFringeRenderer.registerInnerNode')
public registerInnerNode(tag: NodeTag, pre: FringeInnerRenderFunction<Context>, post: FringeInnerRenderFunction<Context>): SimpleFringeRenderer<Context> {
this.internRenderer(FringeType.Pre, tag, this.preRenderers, pre);
this.internRenderer(FringeType.Post, tag, this.postRenderers, post);
@@ -284,7 +292,8 @@ export class FringeWalker<Context> {
this.stream = new FringeStream(fringe(root));
}
public increment(): DocumentNode|undefined {
@traceSync('FringeWalker.increment')
public increment(): DocumentNode | undefined {
const renderInnerNode = (node: AnnotatedFringeNode) => {
if (node.node.leafNode) {
throw new TypeError("Leaf nodes should not be in the Pre/Post position");
@@ -340,7 +349,7 @@ export class TagDynamicEnvironmentEntry {
constructor(
public readonly node: DocumentNode,
public value: any,
public readonly previous: undefined|TagDynamicEnvironmentEntry,
public readonly previous: undefined | TagDynamicEnvironmentEntry,
) {
}
@@ -365,8 +374,9 @@ export class TagDynamicEnvironmentEntry {
* as the restoration of previous values can be handled automatically for us.
*/
export class TagDynamicEnvironment {
private readonly environments = new Map<string, TagDynamicEnvironmentEntry|undefined>();
private readonly environments = new Map<string, TagDynamicEnvironmentEntry | undefined>();
@traceSync('TagDynamicEnvironment.read')
public read(variableName: string): any {
const variableEntry = this.environments.get(variableName);
if (variableEntry) {
@@ -376,6 +386,7 @@ export class TagDynamicEnvironment {
}
}
@traceSync('TagDynamicEnvironment.write')
public write(variableName: string, value: any): any {
const variableEntry = this.environments.get(variableName);
if (variableEntry) {
@@ -385,6 +396,7 @@ export class TagDynamicEnvironment {
}
}
@traceSync('TagDynamicEnvironment.bind')
public bind(variableName: string, node: DocumentNode, value: any): any {
const entry = this.environments.get(variableName);
const newEntry = new TagDynamicEnvironmentEntry(node, value, entry);
@@ -392,6 +404,7 @@ export class TagDynamicEnvironment {
return value
}
@traceSync('TagDynamicEnvironment.pop')
public pop(node: DocumentNode): void {
for (const [variableName, environment] of this.environments.entries()) {
if (Object.is(environment?.node, node)) {
@@ -29,6 +29,7 @@ limitations under the License.
* I'd like to remove the dependency on matrix-bot-sdk.
*/
import { trace, traceSync } from "../../utils";
import { ParameterParser, IArgumentStream, IArgumentListParser, ParsedKeywords, ArgumentStream } from "./ParameterParsing";
import { CommandResult } from "./Validation";
@@ -47,11 +48,11 @@ type CommandLookupEntry<ExecutorType extends BaseFunction> = {
export class CommandTable<ExecutorType extends BaseFunction = BaseFunction> {
private readonly flattenedCommands = new Set<InterfaceCommand<BaseFunction>>();
private readonly commands: CommandLookupEntry<ExecutorType> = { };
private readonly commands: CommandLookupEntry<ExecutorType> = {};
/** Imported tables are tables that "add commands" to this table. They are not sub commands. */
private readonly importedTables = new Set<CommandTable>();
constructor(public readonly name: string|symbol) {
constructor(public readonly name: string | symbol) {
}
@@ -59,6 +60,7 @@ export class CommandTable<ExecutorType extends BaseFunction = BaseFunction> {
* Used to render the help command.
* @returns All of the commands in this table.
*/
@traceSync('CommandTable.getAllCommands')
public getAllCommands(): InterfaceCommand[] {
const importedCommands = [...this.importedTables].reduce((acc, t) => [...acc, ...t.getAllCommands()], []);
return [...this.getExportedCommands(), ...importedCommands]
@@ -67,17 +69,20 @@ export class CommandTable<ExecutorType extends BaseFunction = BaseFunction> {
/**
* @returns Only the commands interned in this table, excludes imported commands.
*/
@traceSync('CommandTable.getExportedCommands')
public getExportedCommands(): InterfaceCommand[] {
return [...this.flattenedCommands.values()];
}
@traceSync('CommandTable.getImportedTables')
public getImportedTables(): CommandTable[] {
return [...this.importedTables];
}
// We use the argument stream so that they can use stream.rest() to get the unconsumed arguments.
@traceSync('CommandTable.findAnExportedMatchingCommand')
public findAnExportedMatchingCommand(stream: IArgumentStream) {
const tableHelper = (table: CommandLookupEntry<ExecutorType>, argumentStream: IArgumentStream): undefined|InterfaceCommand<ExecutorType> => {
const tableHelper = (table: CommandLookupEntry<ExecutorType>, argumentStream: IArgumentStream): undefined | InterfaceCommand<ExecutorType> => {
if (argumentStream.peekItem() === undefined || typeof argumentStream.peekItem() !== 'string') {
// Then they might be using something like "!mjolnir status"
return table.current;
@@ -93,7 +98,8 @@ export class CommandTable<ExecutorType extends BaseFunction = BaseFunction> {
return tableHelper(this.commands, stream);
}
public findAMatchingCommand(stream: IArgumentStream): InterfaceCommand|undefined {
@traceSync('CommandTable.findAMatchingCommand')
public findAMatchingCommand(stream: IArgumentStream): InterfaceCommand | undefined {
const possibleExportedCommand = stream.savingPositionIf({
body: (s: IArgumentStream) => this.findAnExportedMatchingCommand(s),
predicate: command => command === undefined,
@@ -102,7 +108,7 @@ export class CommandTable<ExecutorType extends BaseFunction = BaseFunction> {
return possibleExportedCommand;
}
for (const table of this.importedTables.values()) {
const possibleCommand: InterfaceCommand|undefined = stream.savingPositionIf<InterfaceCommand|undefined>({
const possibleCommand: InterfaceCommand | undefined = stream.savingPositionIf<InterfaceCommand | undefined>({
body: (s: IArgumentStream) => table.findAMatchingCommand(s),
predicate: command => command === undefined,
});
@@ -135,6 +141,7 @@ export class CommandTable<ExecutorType extends BaseFunction = BaseFunction> {
internCommandHelper(this.commands, [...command.designator]);
}
@traceSync('CommandTable.importTable')
public importTable(table: CommandTable): void {
for (const command of table.getAllCommands()) {
if (this.findAMatchingCommand(new ArgumentStream(command.designator))) {
@@ -145,8 +152,8 @@ export class CommandTable<ExecutorType extends BaseFunction = BaseFunction> {
}
}
const COMMAND_TABLE_TABLE = new Map<string|symbol, CommandTable<BaseFunction>>();
export function defineCommandTable(name: string|symbol): CommandTable {
const COMMAND_TABLE_TABLE = new Map<string | symbol, CommandTable<BaseFunction>>();
export function defineCommandTable(name: string | symbol): CommandTable {
if (COMMAND_TABLE_TABLE.has(name)) {
throw new TypeError(`A table called ${name.toString()} already exists`);
}
@@ -155,7 +162,7 @@ export function defineCommandTable(name: string|symbol): CommandTable {
return table;
}
export function findCommandTable<ExecutorType extends BaseFunction>(name: string|symbol): CommandTable<ExecutorType> {
export function findCommandTable<ExecutorType extends BaseFunction>(name: string | symbol): CommandTable<ExecutorType> {
const entry = COMMAND_TABLE_TABLE.get(name);
if (!entry) {
throw new TypeError(`Couldn't find a table called ${name.toString()}`);
@@ -166,7 +173,7 @@ export function findCommandTable<ExecutorType extends BaseFunction>(name: string
/**
* Used to find a table command at the internal DSL level, not as a client for commands.
*/
export function findTableCommand<ExecutorType extends BaseFunction>(tableName: string|symbol, ...designator: string[]): InterfaceCommand<ExecutorType> {
export function findTableCommand<ExecutorType extends BaseFunction>(tableName: string | symbol, ...designator: string[]): InterfaceCommand<ExecutorType> {
const table = findCommandTable(tableName);
const command = table.findAMatchingCommand(new ArgumentStream(designator));
if (command === undefined || !designator.every(part => command.designator.includes(part))) {
@@ -190,14 +197,17 @@ export class InterfaceCommand<ExecutorType extends BaseFunction = BaseFunction>
// Really, surely this should be part of invoke?
// probably... it's just that means that invoke has to return the validation result lol.
// Though this makes no sense if parsing is part of finding a matching command.
@trace('CommandTable.parseArguments')
public async parseArguments(stream: IArgumentStream): ReturnType<ParameterParser> {
return await this.argumentListParser.parse(stream);
}
@traceSync('CommandTable.invoke')
public invoke(context: ThisParameterType<ExecutorType>, ...args: Parameters<ExecutorType>): ReturnType<ExecutorType> {
return this.command.apply(context, args);
}
@trace('CommandTable.parseThenInvoke')
public async parseThenInvoke(context: ThisParameterType<ExecutorType>, stream: IArgumentStream): Promise<ReturnType<ExecutorType>> {
const parameterDescription = await this.parseArguments(stream);
if (parameterDescription.isErr()) {
@@ -220,7 +230,7 @@ export class InterfaceCommand<ExecutorType extends BaseFunction = BaseFunction>
// for what each callback is and does for the adaptors to hook into.
export function defineInterfaceCommand<ExecutorType extends BaseFunction>(description: {
parameters: IArgumentListParser,
table: string|symbol,
table: string | symbol,
command: ExecutorType,
designator: string[],
summary: string,
@@ -38,6 +38,7 @@ import { tickCrossRenderer } from "./MatrixHelpRenderer";
import { CommandInvocationRecord, InterfaceAcceptor, PromptableArgumentStream, PromptOptions } from "./PromptForAccept";
import { ParameterDescription } from "./ParameterParsing";
import { matrixPromptForAccept } from "./MatrixPromptForAccept";
import { trace } from "../../utils";
export interface MatrixContext {
client: MatrixSendClient,
@@ -71,6 +72,7 @@ export class MatrixInterfaceAdaptor<C extends MatrixContext, ExecutorType extend
* along with the result of the executor.
* @param args These will be the arguments to the parser function.
*/
@trace('MatrixInterfaceAdaptor.invoke')
public async invoke(executorContext: ThisParameterType<ExecutorType>, matrixContext: C, ...args: ReadItem[]): Promise<void> {
const invocationRecord = new MatrixInvocationRecord<ThisParameterType<ExecutorType>>(this.interfaceCommand, executorContext, matrixContext);
const stream = new PromptableArgumentStream(args, this, invocationRecord);
@@ -89,6 +91,7 @@ export class MatrixInterfaceAdaptor<C extends MatrixContext, ExecutorType extend
// and an error discovered because their is a fault or an error running the command. Though i don't think this is correct
// since any CommandError recieved is an expected error. It means there is no fault. An exception on the other hand does
// so this suggests we should just remove this.
@trace('MatrixInterfaceAdaptor.reportValidationError')
private async reportValidationError(client: MatrixSendClient, roomId: string, event: any, validationError: CommandError): Promise<void> {
LogService.info("MatrixInterfaceCommand", `User input validation error when parsing command ${JSON.stringify(this.interfaceCommand.designator)}: ${validationError.message}`);
if (this.validationErrorHandler) {
@@ -98,6 +101,7 @@ export class MatrixInterfaceAdaptor<C extends MatrixContext, ExecutorType extend
await tickCrossRenderer.call(this, client, roomId, event, CommandResult.Err(validationError));
}
@trace('MatrixInterfaceAdaptor.promptForAccept')
public async promptForAccept<PresentationType = unknown>(parameter: ParameterDescription, invocationRecord: CommandInvocationRecord): Promise<CommandResult<PresentationType>> {
if (!(invocationRecord instanceof MatrixInvocationRecord)) {
throw new TypeError("The MatrixInterfaceAdaptor only supports invocation records that were produced by itself.");
@@ -150,9 +154,9 @@ export function findMatrixInterfaceAdaptor(interfaceCommand: InterfaceCommand<Ba
* @param renderer Render the result of the application command back to a room.
*/
export function defineMatrixInterfaceAdaptor<ExecutorType extends (...args: any) => Promise<any>>(details: {
interfaceCommand: InterfaceCommand<ExecutorType>,
renderer: RendererSignature<MatrixContext, ExecutorType>
}) {
interfaceCommand: InterfaceCommand<ExecutorType>,
renderer: RendererSignature<MatrixContext, ExecutorType>
}) {
internMatrixInterfaceAdaptor(
details.interfaceCommand,
new MatrixInterfaceAdaptor(
@@ -4,13 +4,14 @@
*/
import { MatrixEmitter, MatrixSendClient } from "../../MatrixEmitter";
import { trace, traceSync } from "../../utils";
import { CommandError, CommandResult } from "./Validation";
import { LogService } from "matrix-bot-sdk";
type PresentationByReactionKey = Map<string/*reaction key*/, any/*presentation*/>;
// Returns true if the listener should be kept.
type ReactionPromptListener = (presentation: any) => boolean|void;
type ReactionPromptListener = (presentation: any) => boolean | void;
// Instead of providing a map of reaciton keys to presentations, should instead
// there be provided an object that can quickly be interned and uninterened from the table
@@ -36,6 +37,7 @@ class ReactionHandler {
matrixEmitter.on('room.event', this.handleEvent.bind(this))
}
@traceSync('ReactionHandler.addPresentationsForEvent')
private addPresentationsForEvent(eventId: string, promptRecord: ReactionPromptRecord): void {
const promptRecords = (() => {
let entry = this.promptRecordByEvent.get(eventId);
@@ -48,6 +50,7 @@ class ReactionHandler {
promptRecords.add(promptRecord);
}
@traceSync('ReactionHandler.removePromptRecordForEvent')
private removePromptRecordForEvent(eventId: string, promptRecord: ReactionPromptRecord): void {
const promptRecords = this.promptRecordByEvent.get(eventId);
if (promptRecords !== undefined) {
@@ -58,14 +61,16 @@ class ReactionHandler {
}
}
@trace('ReactionHandler.addBaseReactionsToEvent')
private async addBaseReactionsToEvent(
roomId: string, eventId: string, presentationsByKey: PresentationByReactionKey, limit = 7
roomId: string, eventId: string, presentationsByKey: PresentationByReactionKey, limit = 7
) {
return await [...presentationsByKey.keys()].slice(0, limit)
.reduce((acc, key) => acc.then(_ => this.client.unstableApis.addReactionToEvent(roomId, eventId, key)),
Promise.resolve());
}
@traceSync('ReactionHandler.handleEvent')
private handleEvent(roomId: string, event: { event_id: string, type: string, content: any, sender: string }): void {
// Horrid, would be nice to have some pattern matchy thingo
if (event.type !== 'm.reaction') {
@@ -102,6 +107,7 @@ class ReactionHandler {
}
}
@trace('ReactionHandler.waitForReactionToPrompt')
public async waitForReactionToPrompt<T>(
roomId: string, eventId: string, presentationByReaction: PresentationByReactionKey, timeout = 600_000 // ten minutes
): Promise<CommandResult<T, CommandError>> {
@@ -161,6 +167,7 @@ export class PromptResponseListener {
this.reactionHandler = new ReactionHandler(matrixEmitter, userId, client);
}
@traceSync('PromptResponseListener.indexToReactionKey')
private indexToReactionKey(index: number): string {
return `${(index + 1).toString()}.`;
}
@@ -168,6 +175,7 @@ export class PromptResponseListener {
// This won't work, we have to have a special key in the original event
// that means we should be waiting for it, that can't be abused/forged.
// As we can't have the event id AOT.
@trace('PromptResponseListener.waitForPresentationList')
public async waitForPresentationList<T>(presentations: T[], roomId: string, eventPromise: Promise<string>): Promise<CommandResult<T>> {
const presentationByReactionKey = presentations.reduce(
(map: PresentationByReactionKey, presentation: T, index: number) => {
@@ -6,6 +6,7 @@
import { EventEmitter } from "stream";
import { MatrixEmitter, MatrixSendClient } from "../../MatrixEmitter";
import { LogService } from "matrix-bot-sdk";
import { trace, traceSync } from "../../utils";
const REACTION_ANNOTATION_KEY = 'ge.applied-langua.ge.draupnir.reaction_handler';
@@ -46,6 +47,7 @@ export class MatrixReactionHandler extends EventEmitter {
* @param roomId The room the event took place in.
* @param event The Matrix event.
*/
@trace('MatrixReactionHandler.handleEvent')
private async handleEvent(roomId: string, event: any): Promise<void> {
if (roomId !== this.roomId) {
return;
@@ -92,6 +94,7 @@ export class MatrixReactionHandler extends EventEmitter {
* Start listening for reactions to events.
* Called normally by an associated mjolnir instance when it is started.
*/
@traceSync('MatrixReactionHandler.start')
public start(emitter: MatrixEmitter): void {
emitter.on('room.event', this.listener);
}
@@ -99,6 +102,7 @@ export class MatrixReactionHandler extends EventEmitter {
/**
* Stop listening for reactions to events.
*/
@traceSync('MatrixReactionHandler.stop')
public stop(emitter: MatrixEmitter): void {
emitter.off('room.event', this.listener);
}
@@ -110,6 +114,7 @@ export class MatrixReactionHandler extends EventEmitter {
* @param additionalContext Any additional context that should be associated with a matrix event for the listener.
* @returns An object that should be deep copied into a the content of a new Matrix event.
*/
@traceSync('MatrixReactionHandler.createAnnotation')
public createAnnotation(listenerName: string, reactionMap: ItemByReactionKey, additionalContext: any = undefined): any {
return {
[REACTION_ANNOTATION_KEY]: {
@@ -127,6 +132,7 @@ export class MatrixReactionHandler extends EventEmitter {
* @param eventId The event id of the event to add reactions to.
* @param reactionMap The reaction map.
*/
@trace('MatrixReactionHandler.addReactionsToEvent')
public async addReactionsToEvent(client: MatrixSendClient, roomId: string, eventId: string, reactionMap: ItemByReactionKey): Promise<void> {
await [...reactionMap.keys()]
.reduce((acc, key) => acc.then(_ => client.unstableApis.addReactionToEvent(roomId, eventId, key)),
@@ -5,6 +5,7 @@
import { RoomAlias } from "matrix-bot-sdk";
import { Permalinks } from "./Permalinks";
import { traceSync, trace } from "../../utils";
type JoinRoom = (roomIdOrAlias: string, viaServers?: string[]) => Promise</*room id*/string>;
type ResolveRoom = (roomIdOrAlias: string) => Promise</* room id */string>
@@ -22,10 +23,12 @@ export abstract class MatrixRoomReference {
}
@traceSync('MatrixRoomReference.toPermalink')
public toPermalink(): string {
return Permalinks.forRoom(this.reference, this.viaServers);
}
@traceSync('MatrixRoomReference.fromAlias')
public static fromAlias(alias: string): MatrixRoomReference {
return new MatrixRoomAlias(alias);
}
@@ -34,6 +37,7 @@ export abstract class MatrixRoomReference {
return new MatrixRoomID(roomId, viaServers);
}
@traceSync('MatrixRoomReference.fromRoomIdOrAlias')
public static fromRoomIdOrAlias(roomIdOrAlias: string, viaServers: string[] = []): MatrixRoomReference {
if (roomIdOrAlias.startsWith('!')) {
return new MatrixRoomID(roomIdOrAlias, viaServers);
@@ -47,6 +51,7 @@ export abstract class MatrixRoomReference {
* @param permalink A permalink to a matrix room.
* @returns A MatrixRoomReference.
*/
@traceSync('MatrixRoomReference.fromPermalink')
public static fromPermalink(permalink: string): MatrixRoomReference {
const parts = Permalinks.parseUrl(permalink);
if (parts.roomIdOrAlias === undefined) {
@@ -62,6 +67,7 @@ export abstract class MatrixRoomReference {
* @param client A client that we can use to resolve the room alias.
* @returns A new MatrixRoomReference that contains the room id.
*/
@trace('MatrixRoomReference.resolve')
public async resolve(client: { resolveRoom: ResolveRoom }): Promise<MatrixRoomID> {
if (this instanceof MatrixRoomID) {
return this;
@@ -77,6 +83,7 @@ export abstract class MatrixRoomReference {
* @param client A matrix client that should join the room.
* @returns A MatrixRoomReference with the room id of the room which was joined.
*/
@trace('MatrixRoomReference.joinClient')
public async joinClient(client: { joinRoom: JoinRoom }): Promise<MatrixRoomID> {
if (this.reference.startsWith('!')) {
await client.joinRoom(this.reference, this.viaServers);
@@ -94,6 +101,7 @@ export abstract class MatrixRoomReference {
* which will be necessary to use if our homeserver hasn't joined the room yet.
* @returns A string representing a room id or alias.
*/
@traceSync('MatrixRoomReference.toRoomIdOrAlias')
public toRoomIdOrAlias(): string {
return this.reference;
}
@@ -3,6 +3,7 @@
* All rights reserved.
*/
import { traceSync } from "../../utils";
import { DocumentNode } from "./DeadDocument";
/**
@@ -26,20 +27,24 @@ export class PagedDuplexStream {
return this.pages.at(this.pages.length - 1)!;
}
@traceSync('PagedDuplexStream.appendToCurrentPage')
private appendToCurrentPage(string: string) {
const currentIndex = this.pages.length - 1;
this.pages[currentIndex] = this.pages[currentIndex] + string;
}
@traceSync('PagedDuplexStream.writeString')
public writeString(string: string): PagedDuplexStream {
this.buffer += string;
return this;
}
@traceSync('PagedDuplexStream.getPosition')
public getPosition(): number {
return this.buffer.length;
}
@traceSync('PagedDuplexStream.isPageAndBufferOverSize')
public isPageAndBufferOverSize(): boolean {
return (this.currentPage.length + this.buffer.length) > this.sizeLimit;
}
@@ -48,6 +53,7 @@ export class PagedDuplexStream {
* Creates a new page from the previously committed text
* @returns A page with all committed text.
*/
@traceSync('PagedDuplexStream.ensureNewPage')
public ensureNewPage(): void {
if (this.currentPage.length !== 0) {
this.pages.push('');
@@ -63,6 +69,7 @@ export class PagedDuplexStream {
* @throws TypeError if the buffer is larger than the `sizeLimit`.
* @returns A page if the buffered text will force the current page to go over the size limit.
*/
@traceSync('PagedDuplexStream.commit')
public commit(node: DocumentNode): void {
if (this.isPageAndBufferOverSize()) {
if (this.currentPage.length === 0 && (this.buffer.length > this.sizeLimit)) {
@@ -75,10 +82,12 @@ export class PagedDuplexStream {
this.lastCommittedNode = node;
}
@traceSync('PagedDuplexStream.getLastCommittedNode')
public getLastCommittedNode(): DocumentNode | undefined {
return this.lastCommittedNode;
}
@traceSync('PagedDuplexStream.peekPage')
public peekPage(): string | undefined {
// We consider a page "ready" when it is no longer the current page.
if (this.pages.length < 2) {
@@ -87,6 +96,7 @@ export class PagedDuplexStream {
return this.pages.at(0);
}
@traceSync('PagedDuplexStream.readPage')
public readPage(): string | undefined {
// We consider a page "ready" when it is no longer the current page.
if (this.pages.length < 2) {
@@ -24,6 +24,7 @@ limitations under the License.
* are NOT distributed, contributed, or committed under the Apache License.
*/
import { trace, traceSync } from "../../utils";
import { ISuperCoolStream, Keyword, ReadItem, SuperCoolStream } from "./CommandReader";
import { PromptOptions } from "./PromptForAccept";
import { CommandError, CommandResult } from "./Validation";
@@ -36,10 +37,12 @@ export interface IArgumentStream extends ISuperCoolStream<ReadItem[]> {
}
export class ArgumentStream extends SuperCoolStream<ReadItem[]> implements IArgumentStream {
@traceSync('ArgumentStream.rest')
public rest() {
return this.source.slice(this.position);
}
@traceSync('ArgumentStream.isPromptable')
public isPromptable(): boolean {
return false;
}
@@ -94,7 +97,7 @@ export function simpleTypeValidator(name: string, predicate: (readItem: ReadItem
}
}
export function presentationTypeOf(presentation: unknown): PresentationType|undefined {
export function presentationTypeOf(presentation: unknown): PresentationType | undefined {
// We have no concept of presentation-subtype
// But we have a top type which is any...
const candidates = [...PRESENTATION_TYPES.values()]
@@ -190,7 +193,7 @@ export class RestDescription<ExecutorContext = unknown> implements ParameterDesc
* argument that can be accepted by a command.
*/
interface KeywordArgumentsDescription {
readonly [prop: string]: KeywordPropertyDescription|undefined;
readonly [prop: string]: KeywordPropertyDescription | undefined;
}
/**
@@ -225,14 +228,15 @@ export class KeywordsDescription {
* A read only map of keywords to their associated properties.
*/
export class ParsedKeywords {
constructor (
constructor(
private readonly descriptions: KeywordArgumentsDescription,
private readonly keywords: ReadonlyMap<string, ReadItem>
) {
}
public getKeyword<T extends ReadItem|boolean>(keyword: string, defaultValue: T|undefined = undefined): T|undefined {
@traceSync('ParsedKeywords.getKeyword')
public getKeyword<T extends ReadItem | boolean>(keyword: string, defaultValue: T | undefined = undefined): T | undefined {
const keywordDescription = this.descriptions[keyword];
if (keywordDescription === undefined) {
throw new TypeError(`${keyword} is not a keyword that has been expected for this command.`);
@@ -258,11 +262,13 @@ class KeywordParser {
) {
}
@traceSync('KeywordParser.getKeywords')
public getKeywords(): ParsedKeywords {
return new ParsedKeywords(this.description.description, this.arguments);
}
@traceSync('KeywordParser.readKeywordAssociatedProperty')
private readKeywordAssociatedProperty(keyword: KeywordPropertyDescription, itemStream: IArgumentStream): CommandResult<any, ArgumentParseError> {
if (itemStream.peekItem() !== undefined && !(itemStream.peekItem() instanceof Keyword)) {
const validationResult = keyword.acceptor.validator(itemStream.peekItem());
@@ -280,6 +286,7 @@ class KeywordParser {
}
}
@traceSync('KeywordParser.parseKeywords')
public parseKeywords(itemStream: IArgumentStream): CommandResult<this> {
while (itemStream.peekItem() !== undefined && itemStream.peekItem() instanceof Keyword) {
const item = itemStream.readItem() as Keyword;
@@ -309,7 +316,8 @@ class KeywordParser {
return CommandResult.Ok(this);
}
public async parseRest(stream: IArgumentStream, shouldPromptForRest = false, restDescription?: RestDescription): Promise<CommandResult<ReadItem[]|undefined>> {
@trace('KeywordParser.parseRest')
public async parseRest(stream: IArgumentStream, shouldPromptForRest = false, restDescription?: RestDescription): Promise<CommandResult<ReadItem[] | undefined>> {
if (restDescription !== undefined) {
return await restDescription.parseRest(stream, shouldPromptForRest, this)
} else {
@@ -332,7 +340,7 @@ export interface ParsedArguments {
readonly keywords: ParsedKeywords,
}
export type Prompt<ExecutorContext> = (this: ExecutorContext, description: ParameterDescription<ExecutorContext>) => Promise<PromptOptions>;
export type Prompt<ExecutorContext> = (this: ExecutorContext, description: ParameterDescription<ExecutorContext>) => Promise<PromptOptions>;
export interface ParameterDescription<ExecutorContext = unknown> {
name: string,
@@ -354,7 +362,7 @@ export type ParameterParser = (stream: IArgumentStream) => Promise<CommandResult
// We should have a new type of CommandResult that accepts a ParamterDescription, and can render what's wrong (e.g. missing parameter).
// Showing where in the item stream it is missing and the command syntax and everything lovely like that.
// How does that work with Union?
export function parameters(descriptions: ParameterDescription[], rest: undefined|RestDescription = undefined, keywords: KeywordsDescription = new KeywordsDescription({}, false)): IArgumentListParser {
export function parameters(descriptions: ParameterDescription[], rest: undefined | RestDescription = undefined, keywords: KeywordsDescription = new KeywordsDescription({}, false)): IArgumentListParser {
return new ArgumentListParser(descriptions, keywords, rest);
}
@@ -377,6 +385,7 @@ class ArgumentListParser implements IArgumentListParser {
) {
}
@trace('ArgumentListParser.parse')
public async parse(stream: IArgumentStream): Promise<CommandResult<ParsedArguments>> {
let hasPrompted = false;
const keywordsParser = this.keywords.getParser();
@@ -430,6 +439,7 @@ export class AbstractArgumentParseError extends CommandError {
super(message)
}
@traceSync('AbstractArgumentParseError.Result')
public static Result<Ok>(message: string, options: { stream: IArgumentStream }): CommandResult<Ok, AbstractArgumentParseError> {
return CommandResult.Err(new AbstractArgumentParseError(options.stream, message));
}
@@ -443,12 +453,14 @@ export class ArgumentParseError extends AbstractArgumentParseError {
super(stream, message)
}
@traceSync('ArgumentParseError.Result')
public static Result<Ok>(message: string, options: { parameter: ParameterDescription, stream: IArgumentStream }): CommandResult<Ok, ArgumentParseError> {
return CommandResult.Err(new ArgumentParseError(options.parameter, options.stream, message));
}
}
export class UnexpectedArgumentError extends AbstractArgumentParseError {
@traceSync('UnexpectedArgumentError.Result')
public static Result<Ok>(message: string, options: { stream: IArgumentStream }): CommandResult<Ok, UnexpectedArgumentError> {
return CommandResult.Err(new UnexpectedArgumentError(options.stream, message));
}
@@ -32,6 +32,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { traceSync } from '../../utils';
/**
* The parts of a permalink.
* @see Permalinks
@@ -67,6 +69,7 @@ export class Permalinks {
private constructor() {
}
@traceSync('Permalinks.encodeViaArgs')
private static encodeViaArgs(servers: string[]): string {
if (!servers || !servers.length) return "";
@@ -79,6 +82,7 @@ export class Permalinks {
* @param {string[]} viaServers The servers to route the permalink through.
* @returns {string} A room permalink.
*/
@traceSync('Permalinks.forRoom')
public static forRoom(roomIdOrAlias: string, viaServers: string[] = []): string {
return `https://matrix.to/#/${encodeURIComponent(roomIdOrAlias)}${Permalinks.encodeViaArgs(viaServers)}`;
}
@@ -88,6 +92,7 @@ export class Permalinks {
* @param {string} userId The user ID to create a permalink for.
* @returns {string} A user permalink.
*/
@traceSync('Permalinks.forUser')
public static forUser(userId: string): string {
return `https://matrix.to/#/${encodeURIComponent(userId)}`;
}
@@ -99,6 +104,7 @@ export class Permalinks {
* @param {string[]} viaServers The servers to route the permalink through.
* @returns {string} An event permalink.
*/
@traceSync('Permalinks.forEvent')
public static forEvent(roomIdOrAlias: string, eventId: string, viaServers: string[] = []): string {
return `https://matrix.to/#/${encodeURIComponent(roomIdOrAlias)}/${encodeURIComponent(eventId)}${Permalinks.encodeViaArgs(viaServers)}`;
}
@@ -108,6 +114,7 @@ export class Permalinks {
* @param {string} matrixTo The matrix.to URL to parse.
* @returns {PermalinkParts} The parts of the permalink.
*/
@traceSync('Permalinks.parseUrl')
public static parseUrl(matrixTo: string): PermalinkParts {
const matrixToRegexp = /^https:\/\/matrix\.to\/#\/(?<entity>[^/?]+)\/?(?<eventId>[^?]+)?(?<query>\?[^]*)?$/;
@@ -3,6 +3,7 @@
* All rights reserved.
*/
import { trace, traceSync } from "../../utils";
import { ReadItem } from "./CommandReader";
import { BaseFunction, InterfaceCommand } from "./InterfaceCommand";
import { ArgumentStream, ParameterDescription } from "./ParameterParsing";
@@ -35,14 +36,18 @@ export class PromptableArgumentStream extends ArgumentStream {
) {
super([...source], start);
}
@traceSync('PromptableArgumentStream.rest')
public rest() {
return this.source.slice(this.position);
}
@traceSync('PromptableArgumentStream.isPromptable')
public isPromptable(): boolean {
return this.interfaceAcceptor.isPromptable
}
@trace('PromptableArgumentStream.prompt')
public async prompt<T = ReadItem>(parameterDescription: ParameterDescription): Promise<CommandResult<T>> {
const result = await this.interfaceAcceptor.promptForAccept(
parameterDescription,
+12 -3
View File
@@ -25,7 +25,9 @@ limitations under the License.
* are NOT distributed, contributed, committed, or licensed under the Apache License.
*/
type ValidationMatchExpression<Ok, Err> = { ok?: (ok: Ok) => any, err?: (err: Err) => any};
import { trace, traceSync } from "../../utils";
type ValidationMatchExpression<Ok, Err> = { ok?: (ok: Ok) => any, err?: (err: Err) => any };
const noValue = Symbol('noValue');
@@ -43,27 +45,33 @@ const noValue = Symbol('noValue');
*/
export class CommandResult<Ok, Err extends CommandError = CommandError> {
private constructor(
private readonly okValue: Ok|typeof noValue,
private readonly errValue: Err|typeof noValue,
private readonly okValue: Ok | typeof noValue,
private readonly errValue: Err | typeof noValue,
) {
}
@traceSync('CommandResult.Ok')
public static Ok<Ok, Err extends CommandError = CommandError>(value: Ok): CommandResult<Ok, Err> {
return new CommandResult<Ok, Err>(value, noValue);
}
@traceSync('CommandResult.Err')
public static Err<Ok, Err extends CommandError = CommandError>(value: Err): CommandResult<Ok, Err> {
return new CommandResult<Ok, Err>(noValue, value);
}
@trace('CommandResult.match')
public async match(expression: ValidationMatchExpression<Ok, Err>) {
return this.okValue ? await expression.ok!(this.ok) : await expression.err!(this.err);
}
@traceSync('CommandResult.isOk')
public isOk(): boolean {
return this.okValue !== noValue;
}
@traceSync('CommandResult.isErr')
public isErr(): boolean {
return this.errValue !== noValue;
}
@@ -98,6 +106,7 @@ export class CommandError {
* @param _options This exists so that the method is extensible by subclasses. Otherwise they wouldn't be able to pass other constructor arguments through this method.
* @returns A CommandResult with a CommandError nested within.
*/
@traceSync('CommandError.Result')
public static Result<Ok>(message: string, _options = {}): CommandResult<Ok> {
return CommandResult.Err(new CommandError(message));
}
+14 -1
View File
@@ -29,6 +29,7 @@ import PolicyList, { ChangeType, ListRuleChange } from "./PolicyList";
import { EntityType, ListRule, Recommendation, RULE_SERVER, RULE_USER } from "./ListRule";
import { LogService, UserID } from "matrix-bot-sdk";
import { ServerAcl } from "./ServerAcl";
import { traceSync } from '../utils';
/**
* The ListRuleCache is a cache for all the rules in a set of lists for a specific entity type and recommendation.
@@ -70,7 +71,7 @@ class ListRuleCache {
* @param entity e.g. an mxid for a user, the server name for a server.
* @returns A single `ListRule` matching the entity.
*/
public getAnyRuleForEntity(entity: string): ListRule|null {
public getAnyRuleForEntity(entity: string): ListRule | null {
const literalRule = this.literalRules.get(entity);
if (literalRule !== undefined) {
return literalRule[0];
@@ -88,6 +89,7 @@ class ListRuleCache {
* Will automatically update with the list.
* @param list A PolicyList.
*/
@traceSync('ListRuleCache.watchList')
public watchList(list: PolicyList): void {
list.on('PolicyList.update', this.listUpdateListener);
const rules = list.rulesOfKind(this.entityType, this.recommendation);
@@ -99,6 +101,7 @@ class ListRuleCache {
* Will stop updating the cache from this list.
* @param list A PolicyList.
*/
@traceSync('ListRuleCache.unwatchList')
public unwatchList(list: PolicyList): void {
list.removeListener('PolicyList.update', this.listUpdateListener);
const rules = list.rulesOfKind(this.entityType, this.recommendation);
@@ -123,6 +126,7 @@ class ListRuleCache {
* Remove a rule from the cache as it is now invalid. e.g. it was removed from a policy list.
* @param rule The rule to remove.
*/
@traceSync('ListRuleCache.uninternRule')
private uninternRule(rule: ListRule) {
/**
* Remove a rule from the map, there may be rules from different lists in the cache.
@@ -151,6 +155,7 @@ class ListRuleCache {
* Add a rule to the cache e.g. it was added to a policy list.
* @param rule The rule to add.
*/
@traceSync('ListRuleCache.internRule')
private internRule(rule: ListRule) {
/**
* Add a rule to the map, there might be duplicates of this rule in other lists.
@@ -175,6 +180,7 @@ class ListRuleCache {
* Update the cache for a single `ListRuleChange`.
* @param change The change made to a rule that was present in the policy list.
*/
@traceSync('ListRuleCache.updateCacheForChange')
private updateCacheForChange(change: ListRuleChange): void {
if (change.rule.kind !== this.entityType || change.rule.recommendation !== this.recommendation) {
return;
@@ -196,6 +202,7 @@ class ListRuleCache {
* Update the cache for a change in a policy list.
* @param changes The changes that were made to list rules since the last update to this policy list.
*/
@traceSync('ListRuleCache.updateCache')
private updateCache(changes: ListRuleChange[]) {
changes.forEach(this.updateCacheForChange, this);
}
@@ -233,12 +240,14 @@ export default class AccessControlUnit {
policyLists.forEach(this.watchList, this);
}
@traceSync('AccessControlUnit.watchList')
public watchList(list: PolicyList) {
for (const cache of this.caches) {
cache.watchList(list);
}
}
@traceSync('AccessControlUnit.unwatchList')
public unwatchList(list: PolicyList) {
for (const cache of this.caches) {
cache.unwatchList(list);
@@ -250,6 +259,7 @@ export default class AccessControlUnit {
* @param domain The server name to test.
* @returns A description of the access that the server has.
*/
@traceSync('AccessControlUnit.getAccessForServer')
public getAccessForServer(domain: string): EntityAccess {
return this.getAccessForEntity(domain, this.serverAllows, this.serverBans);
}
@@ -260,6 +270,7 @@ export default class AccessControlUnit {
* @param policy Whether to check the server part of the user id against server rules.
* @returns A description of the access that the user has.
*/
@traceSync('AccessControlUnit.getAccessForUser')
public getAccessForUser(mxid: string, policy: "CHECK_SERVER" | "IGNORE_SERVER"): EntityAccess {
const userAccess = this.getAccessForEntity(mxid, this.userAllows, this.userBans);
if (userAccess.outcome === Access.Allowed) {
@@ -274,6 +285,7 @@ export default class AccessControlUnit {
}
}
@traceSync('AccessControlUnit.getAccessForEntity')
private getAccessForEntity(entity: string, allowCache: ListRuleCache, bannedCache: ListRuleCache): EntityAccess {
// Check if the entity is explicitly allowed.
// We have to infer that a rule exists for '*' if the allowCache is empty, otherwise you brick the ACL.
@@ -295,6 +307,7 @@ export default class AccessControlUnit {
* @param serverName The name of the server that you are operating from, used to ensure you cannot brick yourself.
* @returns A new `ServerAcl` instance with deny and allow entries created from the rules in this unit.
*/
@traceSync('AccessControlUnit.compileServerAcl')
public compileServerAcl(serverName: string): ServerAcl {
const acl = new ServerAcl(serverName).denyIpAddresses();
const allowedServers = this.serverAllows.allRules;
+3
View File
@@ -26,6 +26,7 @@ limitations under the License.
*/
import { MatrixGlob } from "matrix-bot-sdk";
import { traceSync } from "../utils";
export enum EntityType {
/// `entity` is to be parsed as a glob of users IDs
@@ -143,6 +144,7 @@ export abstract class ListRule {
/**
* Determine whether this rule should apply to a given entity.
*/
@traceSync('ListRule.isMatch')
public isMatch(entity: string): boolean {
return this.glob.test(entity);
}
@@ -150,6 +152,7 @@ export abstract class ListRule {
/**
* @returns Whether the entity in he rule represents a Matrix glob (and not a literal).
*/
@traceSync('ListRule.isGlob')
public isGlob(): boolean {
return /[*?]/.test(this.entity);
}
+4
View File
@@ -2,6 +2,8 @@
* Copyright (C) 2023 Gnuxie <Gnuxie@protonmail.com>
*/
import { trace } from "../utils";
export const SCHEMA_VERSION_KEY = 'ge.applied-langua.ge.draupnir.schema_version';
export type RawSchemedData = object & Record<typeof SCHEMA_VERSION_KEY, unknown>;
@@ -15,6 +17,7 @@ export abstract class MatrixDataManager<Format extends RawSchemedData = RawSchem
protected abstract storeMatixData(data: Format): Promise<void>;
protected abstract createFirstData(): Promise<Format>;
@trace('MatrixDataManager.migrateData')
protected async migrateData(rawData: RawSchemedData): Promise<RawSchemedData> {
const startingVersion = rawData[SCHEMA_VERSION_KEY] as number;
// Rememeber, version 0 has no migrations
@@ -33,6 +36,7 @@ export abstract class MatrixDataManager<Format extends RawSchemedData = RawSchem
}
}
@trace('MatrixDataManager.loadData')
protected async loadData(): Promise<Format> {
const rawData = await this.requestMatrixData();
if (rawData === undefined) {
+17 -3
View File
@@ -31,6 +31,7 @@ import { ALL_RULE_TYPES, EntityType, ListRule, Recommendation, ROOM_RULE_TYPES,
import { MatrixSendClient } from "../MatrixEmitter";
import AwaitLock from "await-lock";
import { monotonicFactory } from "ulidx";
import { trace, traceSync } from "../utils";
/**
* Account data event type used to store the permalinks to each of the policylists.
@@ -156,6 +157,7 @@ export class PolicyList extends EventEmitter {
* @param createRoomOptions Additional room create options such as an alias.
* @returns The room id for the newly created policy list.
*/
@trace('PolicyList.createList')
public static async createList(
client: MatrixSendClient,
shortcode: string,
@@ -178,7 +180,7 @@ export class PolicyList extends EventEmitter {
"state_default": 50,
"users": {
[await client.getUserId()]: 100,
...invite.reduce((users, mxid) => ({...users, [mxid]: 50 }), {}),
...invite.reduce((users, mxid) => ({ ...users, [mxid]: 50 }), {}),
},
"users_default": 0,
};
@@ -193,7 +195,7 @@ export class PolicyList extends EventEmitter {
{
type: SHORTCODE_EVENT_TYPE,
state_key: "",
content: {shortcode: shortcode}
content: { shortcode: shortcode }
}
],
power_level_content_override: powerLevels,
@@ -248,6 +250,7 @@ export class PolicyList extends EventEmitter {
* @param recommendation A specific recommendation to filter for e.g. `m.ban`. Please remember recommendation varients are normalized.
* @returns The active ListRules for the ban list of that kind.
*/
@traceSync('PolicyList.rulesOfKind')
public rulesOfKind(kind: string, recommendation?: Recommendation): ListRule[] {
const rules: ListRule[] = []
const stateKeyMap = this.state.get(kind);
@@ -289,6 +292,7 @@ export class PolicyList extends EventEmitter {
* @param entity The entity to test e.g. the user id, server name or a room id.
* @returns All of the rules that match this entity.
*/
@traceSync('PolicyList.rulesMatchingEntity')
public rulesMatchingEntity(entity: string, ruleKind?: string): ListRule[] {
const ruleTypeOf: (entityPart: string) => string = (entityPart: string) => {
if (ruleKind) {
@@ -322,6 +326,7 @@ export class PolicyList extends EventEmitter {
* @param additionalProperties Any other properties to embed in the rule such as a reason.
* @returns The event id of the policy.
*/
@trace('PolicyList.createPolicy')
public async createPolicy(entityType: EntityType, recommendation: Recommendation, entity: string, additionalProperties = {}): Promise<string> {
// '@' at the beginning of state keys is reserved.
const stateKey = entityType === RULE_USER ? '_' + entity.substring(1) : entity;
@@ -340,6 +345,7 @@ export class PolicyList extends EventEmitter {
* @param entity The entity to ban.
* @param reason A reason we are banning them.
*/
@trace('PolicyList.banEntity')
public async banEntity(ruleType: EntityType, entity: string, reason?: string): Promise<void> {
await this.createPolicy(ruleType, Recommendation.Ban, entity, {
reason: reason || '<no reason supplied>',
@@ -353,6 +359,7 @@ export class PolicyList extends EventEmitter {
* @param entity The entity to unban from this list.
* @returns true if any rules were removed and the entity was unbanned, otherwise false because there were no rules.
*/
@trace('PolicyList.unbanEntity')
public async unbanEntity(ruleType: string, entity: string): Promise<boolean> {
let typesToCheck = [ruleType];
switch (ruleType) {
@@ -377,7 +384,7 @@ export class PolicyList extends EventEmitter {
typesToCheck.map(stateType => this.client.getRoomStateEvent(this.roomId, stateType, stateKey)
.then(_ => stateType) // We need the state type as getRoomState only returns the content, not the top level.
.catch(e => e.statusCode === 404 ? null : Promise.reject(e))))
).filter(e => e); // remove nulls. I don't know why TS still thinks there can be nulls after this??
).filter(e => e); // remove nulls. I don't know why TS still thinks there can be nulls after this??
if (typesToRemove.length === 0) {
return;
}
@@ -393,6 +400,7 @@ export class PolicyList extends EventEmitter {
* and updating the model to reflect the room.
* @returns A description of any rules that were added, modified or removed from the list as a result of this update.
*/
@trace('PolicyList.updateList')
public async updateList(): Promise<ReturnType<PolicyList["updateListWithState"]>> {
await this.updateListLock.acquireAsync();
try {
@@ -409,6 +417,7 @@ export class PolicyList extends EventEmitter {
* @param state Room state to update the list with, provided by `updateList`
* @returns Any changes that have been made to the PolicyList.
*/
@traceSync('PolicyList.updateListWithState')
private updateListWithState(state: any): { revision: Revision, changes: ListRuleChange[] } {
const changes: ListRuleChange[] = [];
for (const event of state) {
@@ -524,6 +533,7 @@ export class PolicyList extends EventEmitter {
* Inform the `PolicyList` about a new event from the room it is modelling.
* @param event An event from the room the `PolicyList` models to inform an instance about.
*/
@traceSync('PolicyList.updateForEvent')
public updateForEvent(eventId: string): void {
if (this.stateByEventId.has(eventId) || this.batchedEvents.has(eventId)) {
return; // we already know about this event.
@@ -555,6 +565,7 @@ class UpdateBatcher {
/**
* Reset the state for the next batch.
*/
@traceSync('UpdateBatcher.reset')
private reset() {
this.latestEventId = null;
this.isWaiting = false;
@@ -566,6 +577,7 @@ class UpdateBatcher {
* and emit a batch.
* @param eventId The id of the first event for this batch.
*/
@trace('UpdateBatcher.checkBatch')
private async checkBatch(eventId: string): Promise<void> {
let start = Date.now();
do {
@@ -580,6 +592,7 @@ class UpdateBatcher {
* Adds an event to the batch.
* @param eventId The event to inform the batcher about.
*/
@traceSync('UpdateBatcher.addToBatch')
public addToBatch(eventId: string): void {
if (this.isWaiting) {
this.latestEventId = eventId;
@@ -625,6 +638,7 @@ export class Revision {
* @param revision The revision we want to check this supersedes.
* @returns True if this Revision supersedes the other revision.
*/
@traceSync('Revision.supersedes')
public supersedes(revision: Revision): boolean {
return this.ulid > revision.ulid;
}
+10
View File
@@ -30,6 +30,7 @@ import { Mjolnir } from "../Mjolnir";
import { MatrixDataManager, RawSchemedData, SCHEMA_VERSION_KEY } from "./MatrixDataManager";
import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference";
import { PolicyList, WATCHED_LISTS_EVENT_TYPE, WARN_UNPROTECTED_ROOM_EVENT_PREFIX } from "./PolicyList";
import { trace, traceSync } from "../utils";
type WatchedListsEvent = RawSchemedData & { references?: string[]; };
/**
@@ -50,6 +51,7 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
return [...this.policyLists];
}
@traceSync("PolicyListManager.resolveListShortcode")
public resolveListShortcode(listShortcode: string): PolicyList | undefined {
return this.lists.find(list => list.listShortcode.toLocaleLowerCase() === listShortcode);
}
@@ -59,6 +61,7 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
* @param roomId The room id for the `PolicyList`.
* @param roomRef A reference (matrix.to URL) for the `PolicyList`.
*/
@trace('PolicyListManager.addPolicyList')
private async addPolicyList(roomId: string, roomRef: string): Promise<PolicyList> {
const list = new PolicyList(roomId, roomRef, this.mjolnir.client);
this.mjolnir.ruleServer?.watch(list);
@@ -76,6 +79,7 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
* @returns The list that has been watched or null if the manager was already
* watching the list.
*/
@trace('PolicyListManager.watchList')
public async watchList(roomRef: MatrixRoomReference): Promise<PolicyList | null> {
const roomId = await roomRef.joinClient(this.mjolnir.client);
if (this.policyLists.find(b => b.roomId === roomId.toRoomIdOrAlias())) {
@@ -99,6 +103,7 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
* @param roomRef A matrix room reference to a list that should be unwatched.
* @returns The list being unwatched or null if we were not watching the list.
*/
@trace('PolicyListManager.unwatchList')
public async unwatchList(roomRef: MatrixRoomReference): Promise<PolicyList | null> {
const roomId = await roomRef.resolve(this.mjolnir.client);
const list = this.policyLists.find(b => b.roomId === roomId.toRoomIdOrAlias()) || null;
@@ -112,10 +117,12 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
return list;
}
@trace('PolicyListManager.createFirstData')
protected async createFirstData(): Promise<RawSchemedData> {
return { [SCHEMA_VERSION_KEY]: 0 };
}
@trace('PolicyListManager.requestMatrixData')
protected async requestMatrixData(): Promise<unknown> {
try {
return await this.mjolnir.client.getAccountData(WATCHED_LISTS_EVENT_TYPE);
@@ -132,6 +139,7 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
/**
* Load the watched policy lists from account data, only used when Mjolnir is initialized.
*/
@trace('PolicyListManager.start')
public async start() {
this.policyLists = [];
const watchedListsEvent = await super.loadData();
@@ -154,6 +162,7 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
/**
* Store to account the list of policy rooms.
*/
@trace('PolicyListManager.storeMatixData')
protected async storeMatixData() {
let list = this.policyLists.map(b => b.roomRef);
await this.mjolnir.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
@@ -171,6 +180,7 @@ export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
*
* @param roomId The id of the room to check/warn.
*/
@trace('PolicyListManager.warnAboutUnprotectedPolicyListRoom')
private async warnAboutUnprotectedPolicyListRoom(roomId: string) {
if (!this.mjolnir.config.protectAllJoinedRooms) {
return; // doesn't matter
+10
View File
@@ -29,6 +29,7 @@ import * as crypto from "crypto";
import { LogService } from "matrix-bot-sdk";
import { EntityType, ListRule } from "./ListRule";
import PolicyList from "./PolicyList";
import { traceSync } from "../utils";
export const USER_MAY_INVITE = 'user_may_invite';
export const CHECK_EVENT_FOR_SPAM = 'check_event_for_spam';
@@ -100,6 +101,7 @@ export default class RuleServer {
* The lower the token, the longer a rule has been tracked for (relative to other rules in this RuleServer).
* The token is incremented before adding new rules to be served.
*/
@traceSync("RuleServer.nextToken")
private nextToken(): void {
this.currentToken += 1;
this.ruleStartsByToken.push([]);
@@ -120,6 +122,7 @@ export default class RuleServer {
* @returns The `EventRules` object describing which rules have been created based on the policy the event represents
* or `undefined` if there are no `EventRules` associated with the event.
*/
@traceSync("RuleServer.getEventRules")
private getEventRules(roomId: string, eventId: string): EventRules | undefined {
return this.rulesByEvent.get(roomId)?.get(eventId);
}
@@ -129,6 +132,7 @@ export default class RuleServer {
* @param eventRules Add rules for an associated policy room event. (e.g. m.policy.rule.user).
* @throws If there are already rules associated with the event specified in `eventRules.eventId`.
*/
@traceSync("RuleServer.addEventRules")
private addEventRules(eventRules: EventRules): void {
const { roomId, eventId, token } = eventRules;
if (this.rulesByEvent.get(roomId)?.has(eventId)) {
@@ -147,6 +151,7 @@ export default class RuleServer {
* Stop serving the rules from this policy rule.
* @param eventRules The EventRules to stop serving from the rule server.
*/
@traceSync("RuleServer.stopEventRules")
private stopEventRules(eventRules: EventRules): void {
const { eventId, roomId, token } = eventRules;
this.rulesByEvent.get(roomId)?.delete(eventId);
@@ -163,6 +168,7 @@ export default class RuleServer {
* Update the rule server to reflect a ListRule change.
* @param change A ListRuleChange sourced from a BanList.
*/
@traceSync("RuleServer.applyRuleChange")
private applyRuleChange(change: ListRuleChange): void {
if (change.changeType === ChangeType.Added) {
const eventRules = new EventRules(change.event.event_id, change.event.room_id, toRuleServerFormat(change.rule), this.currentToken);
@@ -196,6 +202,7 @@ export default class RuleServer {
* as we won't be able to serve rules that have already been interned in the BanList.
* @param banList a BanList to watch for rule changes with.
*/
@traceSync("RuleServer.watch")
public watch(banList: PolicyList): void {
banList.on('PolicyList.update', this.banListUpdateListener);
}
@@ -204,6 +211,7 @@ export default class RuleServer {
* Remove all of the rules that have been created from the policies in this banList.
* @param banList The BanList to unwatch.
*/
@traceSync("RuleServer.unwatch")
public unwatch(banList: PolicyList): void {
banList.removeListener('PolicyList.update', this.banListUpdateListener);
const listRules = this.rulesByEvent.get(banList.roomId);
@@ -221,6 +229,7 @@ export default class RuleServer {
* @param banList The BanList that the changes happened in.
* @param changes An array of ListRuleChanges.
*/
@traceSync("RuleServer.update")
private update(banList: BanList, changes: ListRuleChange[]) {
if (changes.length > 0) {
this.nextToken();
@@ -233,6 +242,7 @@ export default class RuleServer {
* @param sinceToken A token that has previously been issued by this server.
* @returns An object with the rules that have been started and stopped since the token and a new token to poll for more rules with.
*/
@traceSync("RuleServer.getUpdates")
public getUpdates(sinceToken: string | null): { start: RuleServerRule[], stop: string[], reset?: boolean, since: string } {
const updatesSince = <T = EventRules | string>(token: number | null, policyStore: T[][]): T[] => {
if (token === null) {
+11 -1
View File
@@ -26,7 +26,7 @@ limitations under the License.
*/
import { MatrixGlob } from "matrix-bot-sdk";
import { setToArray } from "../utils";
import { setToArray, traceSync } from "../utils";
export interface ServerAclContent {
allow: string[];
@@ -47,6 +47,7 @@ export class ServerAcl {
* Checks the ACL for any entries that might ban ourself.
* @returns A list of deny entries that will not ban our own homeserver.
*/
@traceSync("ServerAcl.safeDeniedServers")
public safeDeniedServers(): string[] {
// The reason we do this check here rather than in the `denyServer` method
// is because `literalAclContent` exists and also we want to be defensive about someone
@@ -61,36 +62,43 @@ export class ServerAcl {
return entries;
}
@traceSync("ServerAcl.allowIpAddresses")
public allowIpAddresses(): ServerAcl {
this.allowIps = true;
return this;
}
@traceSync("ServerAcl.denyIpAddresses")
public denyIpAddresses(): ServerAcl {
this.allowIps = false;
return this;
}
@traceSync("ServerAcl.allowServer")
public allowServer(glob: string): ServerAcl {
this.allowedServers.add(glob);
return this;
}
@traceSync("ServerAcl.setAllowedServers")
public setAllowedServers(globs: string[]): ServerAcl {
this.allowedServers = new Set<string>(globs);
return this;
}
@traceSync("ServerAcl.denyServer")
public denyServer(glob: string): ServerAcl {
this.deniedServers.add(glob);
return this;
}
@traceSync("ServerAcl.setDeniedServers")
public setDeniedServers(globs: string[]): ServerAcl {
this.deniedServers = new Set<string>(globs);
return this;
}
@traceSync("ServerAcl.literalAclContent")
public literalAclContent(): ServerAclContent {
return {
allow: setToArray(this.allowedServers),
@@ -99,6 +107,7 @@ export class ServerAcl {
};
}
@traceSync("ServerAcl.safeAclContent")
public safeAclContent(): ServerAclContent {
const allowed = setToArray(this.allowedServers);
if (!allowed || allowed.length === 0) {
@@ -111,6 +120,7 @@ export class ServerAcl {
};
}
@traceSync("ServerAcl.matches")
public matches(acl: any): boolean {
if (!acl) return false;
+7 -4
View File
@@ -37,6 +37,7 @@ import { ReactionListener } from "../commands/interface-manager/MatrixReactionHa
import { PolicyListManager } from "../models/PolicyListManager";
import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference";
import { findPolicyListFromRoomReference } from "../commands/Ban";
import { trace } from '../utils';
const PROPAGATION_PROMPT_LISTENER = 'ge.applied-langua.ge.draupnir.ban_propagation';
@@ -65,8 +66,8 @@ async function promptBanPropagation(
const reactionMap = makePolicyListShortcodeReferenceMap(mjolnir.policyListManager);
const promptEventId = (await renderMatrixAndSend(
<root>The user {renderMentionPill(event["state_key"], event["content"]?.["displayname"] ?? event["state_key"])} was banned
in <a href={`https://matrix.to/#/${roomId}`}>{roomId}</a> by {new UserID(event["sender"])} for <code>{event["content"]?.["reason"] ?? '<no reason supplied>'}</code>.<br/>
Would you like to add the ban to a policy list?
in <a href={`https://matrix.to/#/${roomId}`}>{roomId}</a> by {new UserID(event["sender"])} for <code>{event["content"]?.["reason"] ?? '<no reason supplied>'}</code>.<br />
Would you like to add the ban to a policy list?
<ol>
{mjolnir.policyListManager.lists.map(list => <li>{list}</li>)}
</ol>
@@ -100,6 +101,7 @@ export class BanPropagation extends Protection {
This will then allow the bot to ban the user from all of your rooms.";
}
@trace("BanPropagation.registerProtection")
public async registerProtection(mjolnir: Mjolnir): Promise<void> {
const listener: ReactionListener = async (key, item, context: BanPropagationMessageContext) => {
try {
@@ -122,13 +124,14 @@ export class BanPropagation extends Protection {
mjolnir.reactionHandler.on(PROPAGATION_PROMPT_LISTENER, listener);
}
@trace("BanPropagation.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (event['type'] !== 'm.room.member' || event['content']?.['membership'] !== 'ban') {
return;
}
if (mjolnir.policyListManager.lists.map(
list => list.rulesMatchingEntity(event['state_key'], RULE_USER)
).some(rules => rules.length > 0)
list => list.rulesMatchingEntity(event['state_key'], RULE_USER)
).some(rules => rules.length > 0)
) {
return; // The user is already banned.
}
+3 -1
View File
@@ -29,6 +29,7 @@ import { Protection } from "./Protection";
import { NumberProtectionSetting } from "./ProtectionSettings";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk";
import { trace } from "../utils";
// if this is exceeded, we'll ban the user for spam and redact their messages
export const DEFAULT_MAX_PER_MINUTE = 10;
@@ -51,6 +52,7 @@ export class BasicFlooding extends Protection {
"banned for spam. This does not publish the ban to any of your ban lists.";
}
@trace("BasicFlooding.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (!this.lastEvents[roomId]) this.lastEvents[roomId] = {};
@@ -63,7 +65,7 @@ export class BasicFlooding extends Protection {
event['origin_server_ts'] = (new Date()).getTime();
}
forUser.push({originServerTs: event['origin_server_ts'], eventId: event['event_id']});
forUser.push({ originServerTs: event['origin_server_ts'], eventId: event['event_id'] });
// Do some math to see if the user is spamming
let messageCount = 0;
+24 -3
View File
@@ -31,6 +31,7 @@ import { Mjolnir } from "../Mjolnir";
import { LogLevel, UserID } from "matrix-bot-sdk";
import { ReadItem } from "../commands/interface-manager/CommandReader";
import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference";
import { trace, traceSync } from "../utils";
const DEFAULT_BUCKET_DURATION_MS = 10_000;
const DEFAULT_BUCKET_NUMBER = 6;
@@ -105,6 +106,7 @@ class TimedHistogram<T> {
* @param now The current date, used to create a new bucket to the event if
* necessary and to determine whether some buckets are too old.
*/
@traceSync("TimedHistogram.push")
push(event: T, now: Date) {
let timeStamp = now.getTime();
let latestBucket = this.buckets[this.buckets.length - 1];
@@ -125,6 +127,7 @@ class TimedHistogram<T> {
* If any buckets are too old, remove them. If there are (still) too
* many buckets, remove the oldest ones.
*/
@traceSync("TimedHistogram.trimBuckets")
private trimBuckets(settings: HistogramSettings, now: Date) {
if (this.buckets.length > settings.bucketNumber) {
this.buckets.splice(0, this.buckets.length - settings.bucketNumber);
@@ -143,6 +146,7 @@ class TimedHistogram<T> {
/**
* Change the settings of a histogram.
*/
@traceSync("TimedHistogram.updateSettings")
public updateSettings(settings: HistogramSettings, now: Date) {
this.trimBuckets(settings, now);
this.settings = settings;
@@ -203,6 +207,7 @@ class Stats {
}
}
@traceSync("Stats.round")
public round(): { min: number, max: number, mean: number, median: number, stddev: number, length: number } {
return {
min: Math.round(this.min),
@@ -229,6 +234,7 @@ class NumbersTimedHistogram extends TimedHistogram<number> {
*
* @returns `null` if the histogram is empty, otherwise `Stats`.
*/
@traceSync("NumbersTimedHistogram.stats")
public stats(): Stats | null {
if (this.buckets.length === 0) {
return null;
@@ -274,11 +280,13 @@ class ServerInfo {
*
* @param lag The duration of lag, in ms.
*/
@traceSync("ServerInfo.pushLag")
pushLag(lag: number, now: Date) {
this.latestMessage = now;
this.histogram.push(lag, now);
}
@traceSync("ServerInfo.updateSettings")
updateSettings(settings: HistogramSettings, now: Date) {
this.histogram.updateSettings(settings, now);
}
@@ -288,6 +296,7 @@ class ServerInfo {
*
* @returns `null` if the histogram is empty, otherwise `Stats`.
*/
@traceSync("ServerInfo.stats")
stats(now?: Date) {
if (now) {
this.latestStatsUpdate = now;
@@ -374,6 +383,7 @@ class RoomInfo {
* @param thresholds The thresholds to use to determine whether an origin server is currently lagging.
* @param now Instant at which all of this was measured.
*/
@traceSync("RoomInfo.pushLag")
pushLag(serverId: string, lag: number, settings: HistogramSettings, thresholds: WarningThresholds, now: Date = new Date()): AlertDiff {
this.latestMessage = now;
@@ -431,6 +441,7 @@ class RoomInfo {
* @returns null if we have no recent data at all,
* some stats otherwise.
*/
@traceSync("RoomInfo.globalStats")
public globalStats(): Stats | null {
return this.totalLag.stats();
}
@@ -444,6 +455,7 @@ class RoomInfo {
*
* @returns `true` is that server is currently on alert.
*/
@traceSync("RoomInfo.isServerOnAlert")
public isServerOnAlert(serverId: string): boolean {
return this.serverAlerts.has(serverId);
}
@@ -451,10 +463,12 @@ class RoomInfo {
/**
* The list of servers currently on alert.
*/
@traceSync("RoomInfo.serversOnAlert")
public serversOnAlert(): IterableIterator<string> {
return this.serverAlerts.keys();
}
@traceSync("RoomInfo.cleanup")
public cleanup(settings: HistogramSettings, now: Date, oldest: Date) {
// Cleanup global histogram.
//
@@ -539,6 +553,7 @@ export class DetectFederationLag extends Protection {
this.settings.bucketDuration.on("set", () => this.updateLatestHistogramSettings());
this.settings.bucketNumber.on("set", () => this.updateLatestHistogramSettings());
}
@traceSync("DetectFederationLag.dispose")
dispose() {
this.settings.bucketDuration.removeAllListeners();
this.settings.bucketNumber.removeAllListeners();
@@ -553,6 +568,7 @@ export class DetectFederationLag extends Protection {
/**
* @param now An argument used only by tests, to simulate events taking place at a specific date.
*/
@trace("DetectFederationLag.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any, now: Date = new Date()) {
// First, handle all cases in which we should ignore the event.
if (!this.firstMessage) {
@@ -672,6 +688,7 @@ export class DetectFederationLag extends Protection {
* @param now Now.
* @param oldest Prune any data older than `oldest`.
*/
@trace("DetectFederationLag.cleanup")
public async cleanup(now: Date = new Date()) {
const oldest: Date = this.getOldestAcceptableData(now);
const lagPerRoomDeleteIds = [];
@@ -689,9 +706,12 @@ export class DetectFederationLag extends Protection {
}
}
@traceSync("DetectFederationLag.getOldestAcceptableData")
private getOldestAcceptableData(now: Date): Date {
return new Date(now.getTime() - this.latestHistogramSettings.bucketDurationMS * this.latestHistogramSettings.bucketNumber)
}
@traceSync("DetectFederationLag.updateLatestHistogramSettings")
private updateLatestHistogramSettings() {
this.latestHistogramSettings = Object.freeze({
bucketDurationMS: this.settings.bucketDuration.value,
@@ -702,7 +722,8 @@ export class DetectFederationLag extends Protection {
/**
* Return (mostly) human-readable lag status.
*/
public async statusCommand(mjolnir: Mjolnir, subcommand: ReadItem[]): Promise<{html: string, text: string} | null> {
@trace("DetectFederationLag.statusCommand")
public async statusCommand(mjolnir: Mjolnir, subcommand: ReadItem[]): Promise<{ html: string, text: string } | null> {
const roomRef = subcommand[0] || "*";
const localDomain = new UserID(await mjolnir.client.getUserId()).domain;
const annotatedStats = (roomInfo: RoomInfo) => {
@@ -755,8 +776,8 @@ export class DetectFederationLag extends Protection {
throw new TypeError(`Unexpected argument ${roomRef}`);
}
return {
text,
html
text,
html
}
}
}
+2 -1
View File
@@ -28,7 +28,7 @@ limitations under the License.
import { Protection } from "./Protection";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk";
import { isTrueJoinEvent } from "../utils";
import { isTrueJoinEvent, trace } from "../utils";
export class FirstMessageIsImage extends Protection {
@@ -49,6 +49,7 @@ export class FirstMessageIsImage extends Protection {
"they'll be banned for spam. This does not publish the ban to any of your ban lists.";
}
@trace("FirstMessageIsImage.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (!this.justJoined[roomId]) this.justJoined[roomId] = [];
+10 -5
View File
@@ -25,10 +25,11 @@ limitations under the License.
* are NOT distributed, contributed, committed, or licensed under the Apache License.
*/
import {Protection} from "./Protection";
import {Mjolnir} from "../Mjolnir";
import {NumberProtectionSetting} from "./ProtectionSettings";
import {LogLevel} from "matrix-bot-sdk";
import { Protection } from "./Protection";
import { Mjolnir } from "../Mjolnir";
import { NumberProtectionSetting } from "./ProtectionSettings";
import { LogLevel } from "matrix-bot-sdk";
import { trace, traceSync } from "../utils";
const DEFAULT_MAX_PER_TIMESCALE = 50;
const DEFAULT_TIMESCALE_MINUTES = 60;
@@ -61,6 +62,7 @@ export class JoinWaveShortCircuit extends Protection {
return "If X amount of users join in Y time, set the room to invite-only."
}
@trace("JoinWaveShortCircuit.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any) {
if (event['type'] !== 'm.room.member') {
// Not a join/leave event.
@@ -100,21 +102,24 @@ export class JoinWaveShortCircuit extends Protection {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Setting ${roomId} to invite-only as more than ${this.settings.maxPer.value} users have joined over the last ${this.settings.timescaleMinutes.value} minutes (since ${this.joinBuckets[roomId].lastBucketStart})`, roomId);
if (!mjolnir.config.noop) {
await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"})
await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", { "join_rule": "invite" })
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId);
}
}
}
@traceSync("JoinWaveShortCircuit.hasExpired")
private hasExpired(at: Date): boolean {
return ((new Date()).getTime() - at.getTime()) > this.timescaleMilliseconds()
}
@traceSync("JoinWaveShortCircuit.timescaleMilliseconds")
private timescaleMilliseconds(): number {
return (this.settings.timescaleMinutes.value * ONE_MINUTE)
}
@trace("JoinWaveShortCircuit.statusCommand")
public async statusCommand(mjolnir: Mjolnir, subcommand: string[]): Promise<{ html: string, text: string }> {
const withExpired = subcommand.includes("withExpired");
const withStart = subcommand.includes("withStart");
+2
View File
@@ -29,6 +29,7 @@ import { Protection } from "./Protection";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, UserID } from "matrix-bot-sdk";
import { Permalinks } from "../commands/interface-manager/Permalinks";
import { trace } from "../utils";
export class MessageIsMedia extends Protection {
@@ -45,6 +46,7 @@ export class MessageIsMedia extends Protection {
return "If a user posts an image or video, that message will be redacted. No bans are issued.";
}
@trace("MessageIsMedia.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (event['type'] === 'm.room.message') {
const content = event['content'] || {};
+2
View File
@@ -29,6 +29,7 @@ import { Protection } from "./Protection";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, UserID } from "matrix-bot-sdk";
import { Permalinks } from "../commands/interface-manager/Permalinks";
import { trace } from "../utils";
export class MessageIsVoice extends Protection {
@@ -45,6 +46,7 @@ export class MessageIsVoice extends Protection {
return "If a user posts a voice message, that message will be redacted. No bans are issued.";
}
@trace("MessageIsVoice.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (event['type'] === 'm.room.message' && event['content']) {
if (event['content']['msgtype'] !== 'm.audio') return;
+21 -1
View File
@@ -38,7 +38,7 @@ import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk";
import { ProtectionSettingValidationError } from "./ProtectionSettings";
import { Consequence } from "./consequence";
import { htmlEscape } from "../utils";
import { htmlEscape, trace, traceSync } from "../utils";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
import { RoomUpdateError } from "../models/RoomUpdateError";
import { BanPropagation } from "./BanPropagation";
@@ -86,6 +86,7 @@ class EnabledProtectionsManager extends MatrixDataManager<EnabledProtectionsEven
super()
}
@trace("EnabledProtectionsManager.requestMatrixData")
protected async requestMatrixData(): Promise<unknown> {
try {
return await this.mjolnir.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE);
@@ -99,6 +100,7 @@ class EnabledProtectionsManager extends MatrixDataManager<EnabledProtectionsEven
}
}
@trace("EnabledProtectionsManager.storeMatixData")
protected async storeMatixData(): Promise<void> {
const data: EnabledProtectionsEvent = {
enabled: [...this.enabledProtections],
@@ -107,26 +109,31 @@ class EnabledProtectionsManager extends MatrixDataManager<EnabledProtectionsEven
await this.mjolnir.client.setAccountData(ENABLED_PROTECTIONS_EVENT_TYPE, data);
}
@trace("EnabledProtectionsManager.createFirstData")
protected async createFirstData(): Promise<EnabledProtectionsEvent> {
return { enabled: [], [SCHEMA_VERSION_KEY]: 0 };
}
@traceSync("EnabledProtectionsManager.isEnabled")
public isEnabled(protection: Protection): boolean {
return this.enabledProtections.has(protection.name);
}
@trace("EnabledProtectionsManager.enable")
public async enable(protection: Protection): Promise<void> {
this.enabledProtections.add(protection.name);
protection.enabled = true;
await this.storeMatixData();
}
@trace("EnabledProtectionsManager.disable")
public async disable(protection: Protection): Promise<void> {
this.enabledProtections.delete(protection.name);
protection.enabled = false;
await this.storeMatixData();
}
@trace("EnabledProtectionsManager.start")
public async start(): Promise<void> {
const data = await this.loadData();
for (const protection of data.enabled) {
@@ -155,6 +162,7 @@ export class ProtectionManager {
* Take all the builtin protections, register them to set their enabled (or not) state and
* update their settings with any saved non-default values
*/
@trace('ProtectionManager.start')
public async start() {
await this.enabledProtectionsManager.start();
this.mjolnir.reportManager.on("report.new", this.handleReport.bind(this));
@@ -178,6 +186,7 @@ export class ProtectionManager {
*
* @param protection The protection object we want to register
*/
@trace('ProtectionManager.registerProtection')
public async registerProtection(protection: Protection) {
this._protections.set(protection.name, protection)
protection.enabled = this.enabledProtectionsManager.isEnabled(protection) ?? false;
@@ -195,6 +204,7 @@ export class ProtectionManager {
*
* @param protection The protection object we want to unregister
*/
@traceSync('ProtectionManager.unregisterProtection')
public unregisterProtection(protectionName: string) {
if (!(this._protections.has(protectionName))) {
throw new Error("Failed to find protection by name: " + protectionName);
@@ -210,6 +220,7 @@ export class ProtectionManager {
* @param protectionName Which protection these settings belong to
* @param changedSettings The settings to change and their values
*/
@trace('ProtectionManager.setProtectionSettings')
public async setProtectionSettings(protectionName: string, changedSettings: { [setting: string]: any }): Promise<any> {
const protection = this._protections.get(protectionName);
if (protection === undefined) {
@@ -241,6 +252,7 @@ export class ProtectionManager {
*
* @param name The name of the protection whose settings we're enabling
*/
@trace('ProtectionManager.enableProtection')
public async enableProtection(name: string) {
const protection = this._protections.get(name);
if (protection !== undefined) {
@@ -258,6 +270,7 @@ export class ProtectionManager {
* @return If there is a protection with this name *and* it is enabled,
* return the protection.
*/
@traceSync('ProtectionManager.getProtection')
public getProtection(protectionName: string): Protection | null {
return this._protections.get(protectionName) ?? null;
}
@@ -268,6 +281,7 @@ export class ProtectionManager {
*
* @param name The name of the protection whose settings we're disabling
*/
@trace('ProtectionManager.disableProtection')
public async disableProtection(name: string) {
const protection = this._protections.get(name);
if (protection !== undefined) {
@@ -283,6 +297,7 @@ export class ProtectionManager {
* @param protectionName The name of the protection whose settings we're reading
* @returns Every saved setting for this protectionName that has a valid value
*/
@trace('ProtectionManager.getProtectionSettings')
public async getProtectionSettings(protectionName: string): Promise<{ [setting: string]: any }> {
let savedSettings: { [setting: string]: any } = {}
try {
@@ -317,6 +332,7 @@ export class ProtectionManager {
return validatedSettings;
}
@trace('ProtectionManager.handleConsequences')
private async handleConsequences(protection: Protection, roomId: string, eventId: string, sender: string, consequences: Consequence[]) {
for (const consequence of consequences) {
try {
@@ -350,6 +366,7 @@ export class ProtectionManager {
}
}
@trace('ProtectionManager.handleEvent')
private async handleEvent(roomId: string, event: any) {
if (this.mjolnir.protectedRoomsTracker.getProtectedRooms().includes(roomId)) {
if (event['sender'] === await this.mjolnir.client.getUserId()) return; // Ignore ourselves
@@ -380,10 +397,12 @@ export class ProtectionManager {
}
@traceSync('ProtectionManager.requiredProtectionPermissions')
private requiredProtectionPermissions(): Set<string> {
return new Set(this.enabledProtections.map((p) => p.requiredStatePermissions).flat())
}
@trace('ProtectionManager.hanverifyPermissionsIndleEvent')
public async verifyPermissionsIn(roomId: string): Promise<RoomUpdateError[]> {
const errors: RoomUpdateError[] = [];
const additionalPermissions = this.requiredProtectionPermissions();
@@ -471,6 +490,7 @@ export class ProtectionManager {
return errors;
}
@trace('ProtectionManager.handleReport')
private async handleReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) {
for (const protection of this.enabledProtections) {
await protection.handleReport(this.mjolnir, roomId, reporterId, event, reason);
+21 -10
View File
@@ -27,6 +27,7 @@ limitations under the License.
import { EventEmitter } from "events";
import { default as parseDuration } from "parse-duration";
import { traceSync } from "../utils";
// Define a few aliases to simplify parsing durations.
@@ -36,7 +37,7 @@ parseDuration["weeks"] = parseDuration["week"] = parseDuration["wk"];
parseDuration["months"] = parseDuration["month"];
parseDuration["years"] = parseDuration["year"];
export class ProtectionSettingValidationError extends Error {};
export class ProtectionSettingValidationError extends Error { };
/*
* @param TChange Type for individual pieces of data (e.g. `string`)
@@ -82,6 +83,7 @@ export class AbstractProtectionListSetting<TChange, TValue> extends AbstractProt
* @param data Value to add to the current setting value
* @returns The potential new value of this setting object
*/
@traceSync("AbstractProtectionSetting.addValue")
addValue(data: TChange): TValue {
throw new Error("not Implemented");
}
@@ -92,6 +94,7 @@ export class AbstractProtectionListSetting<TChange, TValue> extends AbstractProt
* @param data Value to remove from the current setting value
* @returns The potential new value of this setting object
*/
@traceSync("AbstractProtectionSetting.removeValue")
removeValue(data: TChange): TValue {
throw new Error("not Implemented");
}
@@ -110,14 +113,16 @@ export class StringListProtectionSetting extends AbstractProtectionListSetting<s
value: string[] = [];
fromString = (data: string): string => data;
validate = (data: string): boolean => true;
@traceSync("StringListProtectionSetting.addValue")
addValue(data: string): string[] {
this.emit("add", data);
this.value.push(data);
return this.value;
}
@traceSync("StringListProtectionSetting.removeValue")
removeValue(data: string): string[] {
this.emit("remove", data);
this.value = this.value.filter(i => i !== data);
this.value = this.value.filter(i => i !== data);
return this.value;
}
}
@@ -126,11 +131,13 @@ export class StringSetProtectionSetting extends AbstractProtectionListSetting<st
value: Set<string> = new Set();
fromString = (data: string): string => data;
validate = (data: string): boolean => true;
@traceSync("StringSetProtectionSetting.addValue")
addValue(data: string): Set<string> {
this.emit("add", data);
this.value.add(data);
return this.value;
}
@traceSync("StringSetProtectionSetting.removeValue")
removeValue(data: string): Set<string> {
this.emit("remove", data);
this.value.delete(data);
@@ -145,13 +152,13 @@ export class MXIDListProtectionSetting extends StringListProtectionSetting {
}
export class NumberProtectionSetting extends AbstractProtectionSetting<number, number> {
min: number|undefined;
max: number|undefined;
min: number | undefined;
max: number | undefined;
constructor(
defaultValue: number,
min: number|undefined = undefined,
max: number|undefined = undefined
defaultValue: number,
min: number | undefined = undefined,
max: number | undefined = undefined
) {
super();
this.setValue(defaultValue);
@@ -159,10 +166,12 @@ export class NumberProtectionSetting extends AbstractProtectionSetting<number, n
this.max = max;
}
@traceSync("NumberProtectionSetting.fromString")
fromString(data: string) {
let number = Number(data);
return isNaN(number) ? undefined : number;
}
@traceSync("NumberProtectionSetting.validate")
validate(data: number) {
return (!isNaN(data)
&& (this.min === undefined || this.min <= data)
@@ -177,18 +186,20 @@ export class NumberProtectionSetting extends AbstractProtectionSetting<number, n
*/
export class DurationMSProtectionSetting extends AbstractProtectionSetting<number, number> {
constructor(
defaultValue: number,
public readonly minMS: number|undefined = undefined,
public readonly maxMS: number|undefined = undefined
defaultValue: number,
public readonly minMS: number | undefined = undefined,
public readonly maxMS: number | undefined = undefined
) {
super();
this.setValue(defaultValue);
}
@traceSync("DurationMSProtectionSetting.fromString")
fromString(data: string) {
let number = parseDuration(data);
return isNaN(number) ? undefined : number;
}
@traceSync("DurationMSProtectionSetting.validate")
validate(data: number) {
return (!isNaN(data)
&& (this.minMS === undefined || this.minMS <= data)
+2
View File
@@ -28,6 +28,7 @@ limitations under the License.
import { Protection } from "./Protection";
import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings";
import { Mjolnir } from "../Mjolnir";
import { trace } from "../utils";
const MAX_REPORTED_EVENT_BACKLOG = 20;
@@ -57,6 +58,7 @@ export class TrustedReporters extends Protection {
return "Count reports from trusted reporters and take a configured action";
}
@trace("TrustedReporters.handleReport")
public async handleReport(mjolnir: Mjolnir, roomId: string, reporterId: string, event: any, reason?: string): Promise<any> {
if (!this.settings.mxids.value.includes(reporterId)) {
// not a trusted user, we're not interested
+3 -2
View File
@@ -29,13 +29,13 @@ import { Protection } from "./Protection";
import { ConsequenceBan, ConsequenceRedact } from "./consequence";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk";
import { isTrueJoinEvent } from "../utils";
import { isTrueJoinEvent, trace } from "../utils";
export class WordList extends Protection {
settings = {};
private justJoined: { [roomId: string]: { [username: string]: Date} } = {};
private justJoined: { [roomId: string]: { [username: string]: Date } } = {};
private badWords?: RegExp;
constructor() {
@@ -50,6 +50,7 @@ export class WordList extends Protection {
"will be banned from that room. This will not publish the ban to a ban list.";
}
@trace("WordList.handleEvent")
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
const content = event['content'] || {};
+6 -1
View File
@@ -27,7 +27,7 @@ limitations under the License.
import { LogLevel, MatrixClient } from "matrix-bot-sdk"
import { ERROR_KIND_FATAL } from "../ErrorCache";
import { RoomUpdateError } from "../models/RoomUpdateError";
import { redactUserMessagesIn } from "../utils";
import { redactUserMessagesIn, trace, traceSync } from "../utils";
import ManagementRoomOutput from "../ManagementRoomOutput";
import { MatrixSendClient } from "../MatrixEmitter";
@@ -59,11 +59,13 @@ export class RedactUserInRoom implements QueuedRedaction {
this.roomId = roomId;
}
@trace("RedactUserInRoom.redact")
public async redact(client: MatrixClient, managementRoom: ManagementRoomOutput) {
await managementRoom.logMessage(LogLevel.DEBUG, "Mjolnir", `Redacting events from ${this.userId} in room ${this.roomId}.`);
await redactUserMessagesIn(client, managementRoom, this.userId, [this.roomId]);
}
@traceSync("RedactUserInRoom.redactionEqual")
public redactionEqual(redaction: QueuedRedaction): boolean {
if (redaction instanceof RedactUserInRoom) {
return redaction.userId === this.userId && redaction.roomId === this.roomId;
@@ -86,6 +88,7 @@ export class EventRedactionQueue {
* @param redaction a QueuedRedaction.
* @returns True if the queue already has the redaction, false otherwise.
*/
@traceSync("EventRedactionQueue.has")
public has(redaction: QueuedRedaction): boolean {
return !!this.toRedact.get(redaction.roomId)?.find(r => r.redactionEqual(redaction));
}
@@ -95,6 +98,7 @@ export class EventRedactionQueue {
* @param redaction A `QueuedRedaction` to await processing
* @returns `true` if the redaction was added to the queue, `false` if it is a duplicate of a redaction already present in the queue.
*/
@traceSync("EventRedactionQueue.add")
public add(redaction: QueuedRedaction): boolean {
if (this.has(redaction)) {
return false;
@@ -119,6 +123,7 @@ export class EventRedactionQueue {
* @param limitToRoomId If the roomId is provided, only redactions for that room will be processed.
* @returns A description of any errors encountered by each QueuedRedaction that was processed.
*/
@trace("EventRedactionQueue.process")
public async process(client: MatrixSendClient, managementRoom: ManagementRoomOutput, limitToRoomId?: string): Promise<RoomUpdateError[]> {
const errors: RoomUpdateError[] = [];
const redact = async (currentBatch: QueuedRedaction[]) => {
+9 -3
View File
@@ -25,6 +25,8 @@ limitations under the License.
* are NOT distributed, contributed, committed, or licensed under the Apache License.
*/
import { traceSync } from "../utils";
/**
* Used to keep track of protected rooms so they are always ordered for activity.
*
@@ -38,12 +40,13 @@ export class ProtectedRoomActivityTracker {
/**
* A slot to cache the rooms for `protectedRoomsByActivity` ordered so the most recently active room is first.
*/
private activeRoomsCache: null|string[] = null
private activeRoomsCache: null | string[] = null
/**
* Inform the tracker that a new room is being protected by Mjolnir.
* @param roomId The room Mjolnir is now protecting.
*/
@traceSync("ProtectedRoomActivityTracker.addProtectedRoom")
public addProtectedRoom(roomId: string): void {
this.protectedRoomActivities.set(roomId, /* epoch */ 0);
this.activeRoomsCache = null;
@@ -53,6 +56,7 @@ export class ProtectedRoomActivityTracker {
* Inform the trakcer that a room is no longer being protected by Mjolnir.
* @param roomId The roomId that is no longer being protected by Mjolnir.
*/
@traceSync("ProtectedRoomActivityTracker.removeProtectedRoom")
public removeProtectedRoom(roomId: string): void {
this.protectedRoomActivities.delete(roomId);
this.activeRoomsCache = null;
@@ -64,6 +68,7 @@ export class ProtectedRoomActivityTracker {
* @param event The new event.
*
*/
@traceSync("ProtectedRoomActivityTracker.handleEvent")
public handleEvent(roomId: string, event: any): void {
const last_origin_server_ts = this.protectedRoomActivities.get(roomId);
if (last_origin_server_ts !== undefined && Number.isInteger(event.origin_server_ts)) {
@@ -77,11 +82,12 @@ export class ProtectedRoomActivityTracker {
/**
* @returns A list of protected rooms ids ordered by activity.
*/
@traceSync("ProtectedRoomActivityTracker.protectedRoomsByActivity")
public protectedRoomsByActivity(): string[] {
if (!this.activeRoomsCache) {
this.activeRoomsCache = [...this.protectedRoomActivities]
.sort((a, b) => b[1] - a[1])
.map(pair => pair[0]);
.sort((a, b) => b[1] - a[1])
.map(pair => pair[0]);
}
return this.activeRoomsCache;
}
+7
View File
@@ -27,6 +27,7 @@ limitations under the License.
import { LogLevel } from "matrix-bot-sdk";
import { Mjolnir } from "../Mjolnir";
import { trace, traceSync } from "../utils";
export type Task<T> = (queue: ThrottlingQueue) => Promise<T>;
@@ -67,6 +68,7 @@ export class ThrottlingQueue {
/**
* Stop the queue, make sure we can never use it again.
*/
@traceSync('ThrottlingQueue.dispose')
public dispose() {
this.stop();
this._tasks = null;
@@ -85,6 +87,7 @@ export class ThrottlingQueue {
* @param task Some code to execute.
* @return A promise resolved/rejected once `task` is complete.
*/
@traceSync('ThrottlingQueue.push')
public push<T>(task: Task<T>): Promise<T> {
// Wrap `task` into a `Promise` to inform enqueuer when
// the task is complete.
@@ -111,6 +114,7 @@ export class ThrottlingQueue {
*
* @param durationMS A number of milliseconds to wait until resuming operations.
*/
@traceSync('ThrottlingQueue.block')
public block(durationMS: number) {
if (!this.tasks) {
throw new TypeError("Cannot `block()` on a ThrottlingQueue that has already been disposed of.");
@@ -124,6 +128,7 @@ export class ThrottlingQueue {
*
* Does nothing if the loop is already started.
*/
@traceSync('ThrottlingQueue.start')
private start() {
if (this.timeout) {
// Already started.
@@ -142,6 +147,7 @@ export class ThrottlingQueue {
* Does nothing if the loop is already stopped. A loop stopped with `stop()` may be
* resumed by calling `push()` or `start()`.
*/
@traceSync('ThrottlingQueue.stop')
private stop() {
if (!this.timeout) {
// Already stopped.
@@ -177,6 +183,7 @@ export class ThrottlingQueue {
* 2. Otherwise, execute task.
* 3. Once task is complete (whether succeeded or failed), retrigger the loop.
*/
@trace('ThrottlingQueue.step')
private async step() {
// Pull task.
const task = this.tasks.shift();
+5
View File
@@ -27,6 +27,7 @@ limitations under the License.
import { LogLevel, LogService } from "matrix-bot-sdk";
import { Permalinks } from "../commands/interface-manager/Permalinks";
import { Mjolnir } from "../Mjolnir";
import { trace, traceSync } from "../utils";
/**
* A queue of users who have been flagged for redaction typically by the flooding or image protection.
@@ -41,18 +42,22 @@ export class UnlistedUserRedactionQueue {
constructor() {
}
@traceSync('UnlistedUserRedactionQueue.addUser')
public addUser(userId: string) {
this.usersToRedact.add(userId);
}
@traceSync('UnlistedUserRedactionQueue.removeUser')
public removeUser(userId: string) {
this.usersToRedact.delete(userId);
}
@traceSync('UnlistedUserRedactionQueue.isUserQueued')
public isUserQueued(userId: string): boolean {
return this.usersToRedact.has(userId);
}
@trace('UnlistedUserRedactionQueue.handleEvent')
public async handleEvent(roomId: string, event: any, mjolnir: Mjolnir) {
if (this.isUserQueued(event['sender'])) {
const permalink = Permalinks.forEvent(roomId, event['event_id']);
+6 -3
View File
@@ -28,7 +28,7 @@ limitations under the License.
import { PowerLevelAction } from "matrix-bot-sdk/lib/models/PowerLevelAction";
import { LogService, UserID } from "matrix-bot-sdk";
import { htmlToText } from "html-to-text";
import { htmlEscape } from "../utils";
import { htmlEscape, trace } from "../utils";
import { JSDOM } from 'jsdom';
import { EventEmitter } from 'events';
import { Mjolnir } from "../Mjolnir";
@@ -122,6 +122,7 @@ export class ReportManager extends EventEmitter {
* @param event The event being reported.
* @param reason A reason provided by the reporter.
*/
@trace('ReportManager.handleServerAbuseReport')
public async handleServerAbuseReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) {
this.emit("report.new", { roomId: roomId, reporterId: reporterId, event: event, reason: reason });
if (this.mjolnir.config.displayReports) {
@@ -135,6 +136,7 @@ export class ReportManager extends EventEmitter {
* @param roomId The room in which the reaction took place.
* @param event The reaction.
*/
@trace('ReportManager.handleReaction')
public async handleReaction({ roomId, event }: { roomId: string, event: any }) {
if (event.sender === await this.mjolnir.client.getUserId()) {
// Let's not react to our own reactions.
@@ -296,6 +298,7 @@ export class ReportManager extends EventEmitter {
* @param failureEventId The event to annotate with a "FAIL" in case of failure.
* @param onSuccessRemoveEventId Optionally, an event to remove in case of success (e.g. the confirmation dialog).
*/
@trace('ReportManager.executeAction')
private async executeAction({ label, report, successEventId, failureEventId, onSuccessRemoveEventId, moderationRoomId }: { label: string, report: IReportWithAction, successEventId: string, failureEventId: string, onSuccessRemoveEventId?: string, moderationRoomId: string }) {
let action: IUIAction | undefined = ACTIONS.get(label);
if (!action) {
@@ -707,7 +710,7 @@ class DisplayManager {
// Ignore.
}
let eventContent: { msg: string} | { html: string } | { text: string };
let eventContent: { msg: string } | { html: string } | { text: string };
try {
if (event["type"] === "m.room.encrypted") {
eventContent = { msg: "<encrypted content>" };
@@ -848,7 +851,7 @@ class DisplayManager {
}
// ...insert HTML content
for (let {key, value} of [
for (let { key, value } of [
{ key: 'event-content', value: eventContent },
]) {
let node = document.getElementById(key);
+10
View File
@@ -26,6 +26,7 @@ limitations under the License.
*/
import { Mjolnir, REPORT_POLL_EVENT_TYPE } from "../Mjolnir";
import { trace, traceSync } from "../utils";
import { ReportManager } from './ReportManager';
import { LogLevel } from "matrix-bot-sdk";
@@ -53,6 +54,7 @@ export class ReportPoller {
private manager: ReportManager,
) { }
@traceSync('ReportPoller.schedulePoll')
private schedulePoll() {
if (this.timeout === null) {
/*
@@ -70,6 +72,7 @@ export class ReportPoller {
}
}
@trace('ReportPoller.getAbuseReports')
private async getAbuseReports() {
let response_: {
event_reports: { room_id: string, event_id: string, sender: string, reason: string }[],
@@ -130,6 +133,7 @@ export class ReportPoller {
}
}
@trace('ReportPoller.tryGetAbuseReports')
private async tryGetAbuseReports() {
this.timeout = null;
@@ -141,6 +145,9 @@ export class ReportPoller {
this.schedulePoll();
}
@traceSync('ReportPoller.start')
public start(startFrom: number) {
if (this.timeout === null) {
this.from = startFrom;
@@ -149,6 +156,9 @@ export class ReportPoller {
throw new InvalidStateError("cannot start an already started poll");
}
}
@traceSync('ReportPoller.stop')
public stop() {
if (this.timeout !== null) {
clearTimeout(this.timeout);
+112 -9
View File
@@ -25,6 +25,7 @@ limitations under the License.
* are NOT distributed, contributed, committed, or licensed under the Apache License.
*/
import opentelemetry from "@opentelemetry/api";
import {
LogLevel,
LogService,
@@ -134,7 +135,7 @@ export async function getMessagesByUserIn(client: MatrixSendClient, sender: stri
const isGlob = sender.includes("*");
const roomEventFilter = {
rooms: [roomId],
... isGlob ? {} : {senders: [sender]}
...isGlob ? {} : { senders: [sender] }
};
const matcher = new MatrixGlob(sender);
@@ -167,11 +168,11 @@ export async function getMessagesByUserIn(client: MatrixSendClient, sender: stri
* if `null`, start from the most recent point in the timeline.
* @returns The response part of the `/messages` API, see `BackfillResponse`.
*/
async function backfill(from: string|null): Promise<BackfillResponse> {
async function backfill(from: string | null): Promise<BackfillResponse> {
const qs = {
filter: JSON.stringify(roomEventFilter),
dir: "b",
... from ? { from } : {}
...from ? { from } : {}
};
LogService.info("utils", "Backfilling with token: " + from);
return client.doRequest("GET", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages`, qs);
@@ -195,10 +196,10 @@ export async function getMessagesByUserIn(client: MatrixSendClient, sender: stri
}
// We check that we have the token because rooms/messages is not required to provide one
// and will not provide one when there is no more history to paginate.
let token: string|null = null;
let token: string | null = null;
do {
const bfMessages: BackfillResponse = await backfill(token);
const previousToken: string|null = token;
const previousToken: string | null = token;
token = bfMessages['end'] ?? null;
const events = filterEvents(bfMessages['chunk'] || []);
// If we are using a glob, there may be no relevant events in this chunk.
@@ -287,13 +288,13 @@ function patchMatrixClientForConciseExceptions() {
const method: string | undefined = err.method
? err.method
: "req" in err && err.req instanceof ClientRequest
? err.req.method
: params.method;
? err.req.method
: params.method;
const path: string = err.url
? err.url
: "req" in err && err.req instanceof ClientRequest
? err.req.path
: params.uri ?? '';
? err.req.path
: params.uri ?? '';
let body: unknown = null;
if ("body" in err) {
body = err.body;
@@ -503,3 +504,105 @@ export function initializeSentry(config: IConfig) {
// Set to `true` once we have initialized `Sentry` to ensure
// that we do not attempt to initialize it more than once.
let sentryInitialized = false;
/**
* Adds a nested span around a function
*
* @param target The function thats annotated
* @param context The content of the function
* @returns The result of the function
*/
export function trace(spanName: string) {
return (_target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
return {
get() {
const wrapperFn = (...args: any[]) => {
const tracer = opentelemetry.trace.getTracer(
'draupnir-appservice-tracer'
);
return tracer.startActiveSpan(spanName, async (parentSpan) => {
const result = propertyDescriptor.value.apply(this, args);
parentSpan.end();
return result;
});
}
Object.defineProperty(this, memberName, {
value: wrapperFn,
configurable: true,
writable: true
});
return wrapperFn;
}
}
}
}
/**
* Adds a nested span around a sync function
*
* @param target The function thats annotated
* @param context The content of the function
* @returns The result of the function
*/
export function traceSync(spanName: string) {
return (_target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
return {
get() {
const wrapperFn = (...args: any[]) => {
const tracer = opentelemetry.trace.getTracer(
'draupnir-appservice-tracer'
);
return tracer.startActiveSpan(spanName, (parentSpan) => {
const result = propertyDescriptor.value.apply(this, args);
parentSpan.end();
return result;
});
}
Object.defineProperty(this, memberName, {
value: wrapperFn,
configurable: true,
writable: true
});
return wrapperFn;
}
}
}
}
/**
* Adds a independent span around a function
*
* @param target The function thats annotated
* @param context The content of the function
* @returns The result of the function
*/
export function independentTrace(spanName: string) {
return (_target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
return {
get() {
const wrapperFn = (...args: any[]) => {
const tracer = opentelemetry.trace.getTracer(
'draupnir-appservice-tracer'
);
const span = tracer.startSpan(spanName);
const result = propertyDescriptor.value.apply(this, args);
span.end();
return result;
}
Object.defineProperty(this, memberName, {
value: wrapperFn,
configurable: true,
writable: true
});
return wrapperFn;
}
}
}
}
+8 -3
View File
@@ -31,6 +31,7 @@ import { LogService, MatrixClient } from "matrix-bot-sdk";
import RuleServer from "../models/RuleServer";
import { ReportManager } from "../report/ReportManager";
import { IConfig } from "../config";
import { trace, traceSync } from "../utils";
/**
@@ -44,7 +45,7 @@ export class WebAPIs {
private webController: express.Express = express();
private httpServer?: Server;
constructor(private reportManager: ReportManager, private readonly config: IConfig, private readonly ruleServer: RuleServer|null) {
constructor(private reportManager: ReportManager, private readonly config: IConfig, private readonly ruleServer: RuleServer | null) {
// Setup JSON parsing.
this.webController.use(express.json());
}
@@ -52,6 +53,7 @@ export class WebAPIs {
/**
* Start accepting requests to the Web API.
*/
@trace('WebAPIs.start')
public async start() {
if (!this.config.web.enabled) {
return;
@@ -91,12 +93,13 @@ export class WebAPIs {
}
const ruleServer: RuleServer = this.ruleServer;
this.webController.get(updatesUrl, async (request, response) => {
await this.handleRuleServerUpdate(ruleServer, { request, response, since: request.query.since as string});
await this.handleRuleServerUpdate(ruleServer, { request, response, since: request.query.since as string });
});
LogService.info("WebAPIs", `configuring ${updatesUrl}... DONE`);
}
}
@traceSync('WebAPIs.stop')
public stop() {
if (this.httpServer) {
console.log("Stopping WebAPIs.");
@@ -115,6 +118,7 @@ export class WebAPIs {
* @param request The request. Its body SHOULD hold an object `{reason?: string}`
* @param response The response. Used to propagate HTTP success/error.
*/
@trace('WebAPIs.handleReport')
async handleReport({ roomId, eventId, request, response }: { roomId: string, eventId: string, request: express.Request, response: express.Response }) {
// To display any kind of useful information, we need
//
@@ -135,7 +139,7 @@ export class WebAPIs {
if (authorization) {
[, accessToken] = AUTHORIZATION.exec(authorization)!;
} else if (typeof(request.query["access_token"]) === 'string') {
} else if (typeof (request.query["access_token"]) === 'string') {
// Authentication mechanism 2: Access token as query parameter.
accessToken = request.query["access_token"];
} else {
@@ -206,6 +210,7 @@ export class WebAPIs {
}
}
@trace('WebAPIs.handleRuleServerUpdate')
async handleRuleServerUpdate(ruleServer: RuleServer, { since, request, response }: { since: string, request: express.Request, response: express.Response }) {
// FIXME Have to do this because express sends keep alive by default and during tests.
// The server will never be able to close because express never closes the sockets, only stops accepting new connections.
+1847 -414
View File
File diff suppressed because it is too large Load Diff