diff --git a/config/default.yaml b/config/default.yaml index e9c001bf..2e97c06e 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -62,25 +62,14 @@ recordIgnoredInvites: false # (see verboseLogging to adjust this a bit.) managementRoom: "#moderators:example.org" -# Deprecated and will be removed in a future version. -# Running with verboseLogging is unsupported. -# Whether Draupnir should log a lot more messages in the room, -# mainly involves "all-OK" messages, and debugging messages for when draupnir checks bans in a room. -verboseLogging: false - # The log level of terminal (or container) output, # can be one of DEBUG, INFO, WARN and ERROR, in increasing order of importance and severity. # -# This should be at INFO or DEBUG in order to get support for Draupnir problems. +# This should set to INFO or DEBUG in order to get support for Draupnir issues logLevel: "INFO" -# Whether or not Draupnir should synchronize policy lists immediately after startup. -# Equivalent to running '!draupnir sync'. -syncOnStartup: true - -# Whether or not Draupnir should check moderation permissions in all protected rooms on startup. -# Equivalent to running `!draupnir verify`. -verifyPermissionsOnStartup: true +# FIXME!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# WARN for these options that have been delted. # Whether or not Draupnir should actually apply bans and policy lists, # turn on to trial some untrusted configuration or lists. @@ -90,12 +79,6 @@ noop: false # DO NOT change this to `true` unless you are very confident that you know what you are doing. disableServerACL: false -# Whether Draupnir should check member lists quicker (by using a different endpoint), -# keep in mind that enabling this will miss invited (but not joined) users. -# -# Turn on if your bot is in (very) large rooms, or in large amounts of rooms. -fasterMembershipChecks: false - # A case-insensitive list of ban reasons to have the bot also automatically redact the user's messages for. # # If the bot sees you ban a user with a reason that is an (exact case-insensitive) match to this list, @@ -111,15 +94,6 @@ automaticallyRedactForReasons: - "spam" - "advertising" -# A list of rooms to protect. Draupnir will add this to the list it knows from its account data. -# -# It won't, however, add it to the account data. -# Manually add the room via '!draupnir rooms add' to have it stay protected regardless if this config value changes. -# -# Note: These must be matrix.to URLs -protectedRooms: - - "https://matrix.to/#/#yourroom:example.org" - # Whether or not to add all joined rooms to the "protected rooms" list # (excluding the management room and watched policy list rooms, see below). # diff --git a/src/ConfigSchema.ts b/src/ConfigSchema.ts new file mode 100644 index 00000000..caddda74 --- /dev/null +++ b/src/ConfigSchema.ts @@ -0,0 +1,312 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// SPDX-FileCopyrightText: 2019 2021 The Matrix.org Foundation C.I.C. +// +// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0 +// +// SPDX-FileAttributionText: +// This modified file incorporates work from mjolnir +// https://github.com/matrix-org/mjolnir +// + +import { Type } from "@sinclair/typebox"; +import { EDStatic, Permalink, StringRoomAlias, StringRoomID } from "matrix-protection-suite"; + +const PantalaimonConfig = Type.Object({ + use: Type.Boolean({ + description: "Whether or not Draupnir will use pantalaimon to access the matrix homeserver,\ + set to `true` if you're using pantalaimon.\ + Be sure to point homeserverUrl to the pantalaimon instance.\ + Draupnir will log in using the given username and password once,\ + then store the resulting access token in a file under dataPath.", + }), + username: Type.String({ + description: "The username for Draupnir to log into Pantalaimon with." + }), + password: Type.String({ + description: "The password Draupnir will log into Pantalaimon with.\ + After successfully logging in once, this will be ignored, so this value can be blanked after the first startup.\ + This option can be loaded from a file by passing \"--pantalaimon-password-path \" at the command line,\ + which would allow using secret management systems such as systemd's service credentials." + }), +}, { + description: "Options related to Pantalaimon (https://github.com/matrix-org/pantalaimon)" +}); + +const AdminConfig = Type.Object({ + enableMakeRoomAdminCommand: Type.Boolean({ + description: `Whether or not Draupnir can temporarily take control of any eligible account from the local homeserver who's in the room + (with enough permissions) to "make" a user an admin. + + This only works if a local user with enough admin permissions is present in the room.` + }) +}, { + description: `Server administration commands, these commands will only work if Draupnir is + a global server administrator, and the bot's server is a Synapse instance.` +}); + +const BanCommandConfig = Type.Object({ + defaultReasons: Type.Array(Type.String(), { + description: "The default reasons to be prompted with if the reason is missing from a ban command." + }) +}, { + description: "Options for the ban command." +}); + +const WordlistProtectionConfig = Type.Object({ + words: Type.Array(Type.String(), { + description: `A list of case-insensitive keywords that the WordList protection will watch for from new users. + + WordList will ban users who use these words when first joining a room, so take caution when selecting them. + + For advanced usage, regex can also be used, see the following links for more information; + - https://www.digitalocean.com/community/tutorials/an-introduction-to-regular-expressions + - https://regexr.com/ + - https://regexone.com/` + }), + minutesBeforeTrusting: Type.Number({ + description: `For how long (in minutes) the user is "new" to the WordList plugin. + + After this time, the user will no longer be banned for using a word in the above wordlist. + + Set to zero to disable the timeout and make users *always* appear "new". + (users will always be banned if they say a bad word)` + }) +}, { + description: `Configuration for the wordlist plugin, which can ban users based if they say certain + blocked words shortly after joining.` +}) + +const ProtectionsConfig = Type.Object({ + wordlist: WordlistProtectionConfig +}) + +const CommandsConfig = Type.Object({ + // FIXME: Does this even do anything anymore? + allowNoPrefix: Type.Boolean({ + description: `Whether or not the \`!draupnir\` prefix is necessary to submit commands. + + If \`true\`, will allow commands like \`!ban\`, \`!help\`, etc. + + Note: Draupnir can also be pinged by display name instead of having to use + the !draupnir prefix. For example, "my_moderator_bot: ban @spammer:example.org" + will address only my_moderator_bot.` + }), + additionalPrefixes: Type.Array(Type.String(), { + description: "Any additional bot prefixes that Draupnir will listen to. i.e. adding `mod` will allow `!mod help`." + }), + confirmWildcardBan: Type.Boolean({ + description: `Whether or not commands with a wildcard (*) will require an additional \`--force\` argument + in the command to be able to be submitted.` + }), + ban: BanCommandConfig, +}, { + description: "Misc options for command handling and commands" +}); + +const RoomStateBackingStoreConfig = Type.Object({ + enabled: Type.Boolean(), +}, +{ + description: `The room state backing store writes a copy of the room state for all protected + rooms to the data directory. + It is recommended to enable this option unless you deploy Draupnir close to the + homeserver and know that Draupnir is starting up quickly. If your homeserver can + respond quickly to Draupnir's requests for \`/state\` then you might not need this option.` +}); + +const HealthzConfig = Type.Object({ + enabled: Type.Boolean({ + description: "Whether the healthz integration should be enabled (default false)", + }), + port: Type.Integer({ + description: "The port to expose the webserver on. Defaults to 8080.", + }), + address: Type.String({ + description: "The address to listen for requests on. Defaults to all addresses." + }), + endpoint: Type.String({ + description: "The path to expose the monitoring endpoint at. Defaults to `/healthz`" + }), + healthyStatus: Type.Integer({ + description: `The HTTP status code which reports that the bot is healthy/ready to + process requests. Typically this should not be changed. Defaults to 200.` + }), + unhealthyStatus: Type.Integer({ + description: `The HTTP status code which reports that the bot is not healthy/ready. + Defaults to 418.` + }), +}, { + description: `healthz options. These options are best for use in container environments + like Kubernetes to detect how healthy the service is. The bot will report + that it is unhealthy until it is able to process user requests. Typically + this means that it'll flag itself as unhealthy for a number of minutes + before saying "Now monitoring rooms" and flagging itself healthy.".` +}); + +const SentryConfig = Type.Object({ + dsn: Type.String({ + description: `The key used to upload Sentry data to the server. + dsn: "https://XXXXXXXXX@example.com/YYY` + }), + tracesSampleRate: Type.Number({ + description: `Frequency of performance monitoring. + A number in [0.0, 1.0], where 0.0 means "don't bother with tracing" + and 1.0 means "trace performance at every opportunity".` + }) +}, { + description: `Sentry options. Sentry is a tool used to receive/collate/triage runtime + errors and performance issues. Skip this section if you do not wish to use Sentry.`, +}) + +const HealthConfig = Type.Object({ + healthz: HealthzConfig, + sentry: Type.Optional(Type.Union([SentryConfig, Type.Null()])), +}, { + description: "Options for advanced monitoring of the health of the bot." +}) + +const AbuseReportingConfig = Type.Object({ + enabled: Type.Boolean({ + description: "Whether to enable the abuse reporting feature." + }) +}, { + description: `A web API designed to intercept Matrix API + POST /_matrix/client/r0/rooms/{roomId}/report/{eventId} + and display readable abuse reports in the moderation room. + + If you wish to take advantage of this feature, you will need + to configure a reverse proxy, see e.g. test/nginx.conf` +}) + +const WebConfig = Type.Object({ + enabled: Type.Boolean({ + description: "Whether to enable web APIs.", + }), + port: Type.Integer({ + description: "The port to expose the webserver on. Defaults to 8080." + }), + address: Type.String({ + description: "The address to listen for requests on. Defaults to only the current computer." + }), + abuseReporting: AbuseReportingConfig, + +}, { + description: "Options for exposing web APIs." +}) + +// FIXME: Would be nice if MPS had a transform for MatrixRoomReferences +export const ConfigSchema = Type.Object({ + homeserverUrl: Type.String({ + // I hate this? does matrix-bot-sdk know to use well knoen? what's going on here + description: "The URL to connect to the client server API with, this could be Pantalaimon." + }), + rawHomeserverUrl: Type.String({ + description: "The publical facing URL client server API and /_synapse/ are served under, note that this must not be the URL for Pantalaimon" + }), + accessToken: Type.String({ + description: ` + A Matrix access token for the Matrix user to authenticate with. + Draupnir will only use this if pantalaimon.use is false. + This option can be loaded from a file by passing "--access-token-path " at the command line, + which would allow using secret management systems such as systemd's service credentials.`, + }), + pantalaimon: PantalaimonConfig, + experimentalRustCrypto: Type.Boolean({ + description: "\ + Experimental usage of the matrix-bot-sdk rust crypto.\ + This can not be used with Pantalaimon.\ + Make sure to setup the bot as if you are NOT using pantalaimon for this.\ + Warning: At this time this is not considered production safe." + }), + dataPath: Type.String({ + description: 'The path Draupnir will store its state/data in, leave default ("/data/storage") when using containers.' + }), + autojoinOnlyIfManager: Type.Boolean({ + description: "If true (the default), Draupnir will only accept invites from users present in managementRoom." + }), + acceptInvitesFromSpace: Type.Union([Permalink, StringRoomID, StringRoomAlias], { + description: "If `autojoinOnlyIfManager` is false, only the members in this space can invite the bot to protect new rooms." + }), + recordIgnoredInvites: Type.Boolean({ + description: "Whether Draupnir should report ignored invites to the management room (if autojoinOnlyIfManager is true)." + }), + managementRoom: Type.Union([Permalink, StringRoomID, StringRoomAlias], { + description: "The room ID (or room alias) of the management room, anyone in this room can issue commands to Draupnir.\ + Draupnir has no more granular access controls other than this, be sure you trust everyone in this room - secure it!\ + This should be a room alias or room ID - not a matrix.to URL.\ + Note: By default, Draupnir is fairly verbose - expect a lot of messages in this room.\ + (see verboseLogging to adjust this a bit.)" + }), + logLevel: Type.Union([ + Type.Literal("DEBUG"), + Type.Literal("INFO"), + Type.Literal("WARN"), + Type.Literal("ERROR") + ], + { + description: "The log level of terminal (or container) output,\ + can be one of DEBUG, INFO, WARN and ERROR, in increasing order of importance and severity.\ + This should set to INFO or DEBUG in order to get support for Draupnir issues." + }), + logMutedModules: Type.Array(Type.String(), { + description: "Modules to mute in the log output" + }), + noop: Type.Boolean({ + description: "Whether or not Draupnir should actually apply bans and policy lists,\ + turn on to trial some untrusted configuration or lists." + }), + disableServerACL: Type.Boolean({ + description: "# Whether or not Draupnir should apply `m.room.server_acl` events.\ + DO NOT change this to `true` unless you are very confident that you know what you are doing." + }), + automaticallyRedactForReasons: Type.Array(Type.String(), { + description: `A case-insensitive list of ban reasons to have the bot also automatically redact the user's messages for. + + If the bot sees you ban a user with a reason that is an (exact case-insensitive) match to this list, + it will also remove the user's messages automatically. + # + Typically this is useful to avoid having to give two commands to the bot. + Advanced: Use asterisks to have the reason match using "globs" + (f.e. "spam*testing" would match "spam for testing" as well as "spamtesting"). + + See here for more info: https://www.digitalocean.com/community/tools/glob + Note: Keep in mind that glob is NOT regex!` + }), + protectAllJoinedRooms: Type.Boolean({ + description: `Whether or not to add all joined rooms to the "protected rooms" list + (excluding the management room and watched policy list rooms, see below). + + Note that this effectively makes the protectedRooms and associated commands useless + for regular rooms. + + Note: the management room is *excluded* from this condition. + Explicitly add it as a protected room to protect it. + + Note: Ban list rooms the bot is watching but didn't create will not be protected. + Explicitly add these rooms as a protected room list if you want them protected.` + }), + backgroundDelayMS: Type.Integer({ + description: `Increase this delay to have Draupnir wait longer between two consecutive backgrounded + operations. The total duration of operations will be longer, but the homeserver won't + be affected as much. Conversely, decrease this delay to have Draupnir chain operations + faster. The total duration of operations will generally be shorter, but the performance + of the homeserver may be more impacted.` + }), + pollReports: Type.Boolean({ + description: `Whether or not to actively poll synapse for abuse reports, to be used + instead of intercepting client calls to synapse's abuse endpoint, when that + isn't possible/practical.` + }), + displayReports: Type.Boolean({ + description: `Whether or not new reports, received either by webapi or polling, + should be printed to our managementRoom.` + }), + admin: Type.Optional(AdminConfig), + commands: CommandsConfig, + protections: ProtectionsConfig, + roomStateBackingStore: RoomStateBackingStoreConfig, + health: HealthConfig, + web: WebConfig, +}); +// eslint-disable-next-line no-redeclare +export type ConfigSchema = EDStatic; diff --git a/src/Draupnir.ts b/src/Draupnir.ts index 92f58c8a..59c0a179 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -25,7 +25,7 @@ limitations under the License. * are NOT distributed, contributed, committed, or licensed under the Apache License. */ -import { ActionResult, Client, ClientPlatform, ClientRooms, EventReport, LoggableConfigTracker, Logger, MatrixRoomID, MatrixRoomReference, MembershipEvent, Ok, PolicyRoomManager, ProtectedRoomsSet, RoomEvent, RoomMembershipManager, RoomMembershipRevisionIssuer, RoomMessage, RoomStateManager, StringRoomID, StringUserID, Task, TextMessageContent, Value, isError, isStringRoomAlias, isStringRoomID, serverName, userLocalpart } from "matrix-protection-suite"; +import { ActionResult, Client, ClientPlatform, ClientRooms, EventReport, LoggableConfigTracker, Logger, MatrixRoomID, MembershipEvent, Ok, PolicyRoomManager, ProtectedRoomsSet, RoomEvent, RoomMembershipManager, RoomMembershipRevisionIssuer, RoomMessage, RoomStateManager, StringRoomID, StringUserID, Task, TextMessageContent, Value, isError, serverName, userLocalpart } from "matrix-protection-suite"; import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue"; import { findCommandTable } from "./commands/interface-manager/InterfaceCommand"; import { ThrottlingQueue } from "./queues/ThrottlingQueue"; @@ -104,7 +104,7 @@ export class Draupnir implements Client { ) { this.managementRoomID = this.managementRoom.toRoomIDOrAlias(); this.managementRoomOutput = new ManagementRoomOutput( - this.managementRoomID, this.clientUserID, this.client, this.config + this.managementRoomID, this.clientUserID, this.client ); this.reactionHandler = new MatrixReactionHandler(this.managementRoom.toRoomIDOrAlias(), client, clientUserID, clientPlatform); this.reportManager = new ReportManager(this); @@ -154,18 +154,7 @@ export class Draupnir implements Client { if (config.acceptInvitesFromSpace === undefined) { throw new TypeError(`You cannot leave config.acceptInvitesFromSpace undefined if you have disabled config.autojoinOnlyIfManager`); } - const room = (() => { - if (isStringRoomID(config.acceptInvitesFromSpace) || isStringRoomAlias(config.acceptInvitesFromSpace)) { - return config.acceptInvitesFromSpace; - } else { - const parseResult = MatrixRoomReference.fromPermalink(config.acceptInvitesFromSpace); - if (isError(parseResult)) { - throw new TypeError(`config.acceptInvitesFromSpace: ${config.acceptInvitesFromSpace} needs to be a room id, alias or permalink`); - } - return parseResult.ok; - } - })(); - return await clientPlatform.toRoomJoiner().joinRoom(room); + return await clientPlatform.toRoomJoiner().joinRoom(config.acceptInvitesFromSpace); } })(); if (isError(acceptInvitesFromRoom)) { diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 889a5e7f..21520f09 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -28,10 +28,7 @@ limitations under the License. import { isError, StringUserID, - MatrixRoomReference, isStringUserID, - isStringRoomAlias, - isStringRoomID, StandardClientsInRoomMap, DefaultEventDecoder, setGlobalLoggerProvider, @@ -42,8 +39,7 @@ import { ClientCapabilityFactory, MatrixSendClient, RoomStateManagerFactory, - SafeMatrixEmitter, - resolveRoomReferenceSafe + SafeMatrixEmitter } from 'matrix-protection-suite-for-matrix-bot-sdk'; import { IConfig } from "./config"; import { Draupnir } from "./Draupnir"; @@ -74,15 +70,7 @@ export async function makeDraupnirBotModeFromConfig( if (!isStringUserID(clientUserId)) { throw new TypeError(`${clientUserId} is not a valid mxid`); } - if (!isStringRoomAlias(config.managementRoom) && !isStringRoomID(config.managementRoom)) { - throw new TypeError(`${config.managementRoom} is not a valid room id or alias`); - } - const configManagementRoomReference = MatrixRoomReference.fromRoomIDOrAlias(config.managementRoom); - const managementRoom = await resolveRoomReferenceSafe(client, configManagementRoomReference); - if (isError(managementRoom)) { - throw managementRoom.error; - } - await client.joinRoom(managementRoom.ok.toRoomIDOrAlias(), managementRoom.ok.getViaServers()); + const clientsInRoomMap = new StandardClientsInRoomMap(); const clientProvider = async (userID: StringUserID) => { if (userID !== clientUserId) { @@ -103,6 +91,11 @@ export async function makeDraupnirBotModeFromConfig( clientProvider, roomStateManagerFactory ); + const roomJoiner = clientCapabilityFactory.makeClientPlatform(clientUserId, client).toRoomJoiner(); + const managementRoom = await roomJoiner.joinRoom(config.managementRoom); + if (isError(managementRoom)) { + throw managementRoom.error; + } const draupnir = await draupnirFactory.makeDraupnir( clientUserId, managementRoom.ok, diff --git a/src/ManagementRoomOutput.ts b/src/ManagementRoomOutput.ts index 757a4189..30a88ab6 100644 --- a/src/ManagementRoomOutput.ts +++ b/src/ManagementRoomOutput.ts @@ -27,7 +27,6 @@ limitations under the License. import * as Sentry from "@sentry/node"; import { LogLevel, LogService, MessageType, TextualMessageEventContent } from "matrix-bot-sdk"; -import { IConfig } from "./config"; import { htmlEscape } from "./utils"; import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; import { Permalinks, StringRoomAlias, StringRoomID, StringUserID, serverName } from "matrix-protection-suite"; @@ -47,8 +46,7 @@ export default class ManagementRoomOutput { constructor( private readonly managementRoomID: StringRoomID, private readonly clientUserID: StringUserID, - private readonly client: MatrixSendClient, - private readonly config: IConfig, + private readonly client: MatrixSendClient ) { } @@ -115,7 +113,7 @@ export default class ManagementRoomOutput { if (!additionalRoomIds) additionalRoomIds = []; if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds]; - if (this.config.verboseLogging || LogLevel.INFO.includes(level)) { + if (LogLevel.INFO.includes(level)) { let clientMessage = message; if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`; if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`; diff --git a/src/config.ts b/src/config.ts index ed5f9d24..5e916e89 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,6 +30,11 @@ import { load } from "js-yaml"; import { MatrixClient, LogService } from "matrix-bot-sdk"; import Config from "config"; import path from "path"; +import { ConfigSchema } from "./ConfigSchema"; +import { ActionResult, DecodeException, Logger, StringRoomID, Value, isError } from "matrix-protection-suite"; +import { ValueError } from "@sinclair/typebox/errors" + +const log = new Logger('config'); /** * The configuration, as read from production.yaml @@ -39,106 +44,7 @@ import path from "path"; // The object is magically generated by external lib `config` // from the file specified by `NODE_ENV`, e.g. production.yaml // or harness.yaml. -export interface IConfig { - homeserverUrl: string; - rawHomeserverUrl: string; - accessToken: string; - pantalaimon: { - use: boolean; - username: string; - password: string; - }; - dataPath: string; - /** - * If true, Mjolnir will only accept invites from users present in managementRoom. - * Otherwise a space must be provided to `acceptInvitesFromSpace`. - */ - autojoinOnlyIfManager: boolean; - /** Mjolnir will accept invites from members of this space if `autojoinOnlyIfManager` is false. */ - acceptInvitesFromSpace: string; - recordIgnoredInvites: boolean; - managementRoom: string; - verboseLogging: boolean; - logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; - logMutedModules: string[], - syncOnStartup: boolean; - verifyPermissionsOnStartup: boolean; - disableServerACL: boolean; - noop: boolean; - protectedRooms: string[]; // matrix.to urls - fasterMembershipChecks: boolean; - automaticallyRedactForReasons: string[]; // case-insensitive globs - protectAllJoinedRooms: boolean; - /** - * Backgrounded tasks: number of milliseconds to wait between the completion - * of one background task and the start of the next one. - */ - backgroundDelayMS: number; - pollReports: boolean; - /** - * Whether or not new reports, received either by webapi or polling, - * should be printed to our managementRoom. - */ - displayReports: boolean; - admin?: { - enableMakeRoomAdminCommand?: boolean; - } - commands: { - allowNoPrefix: boolean; - additionalPrefixes: string[]; - confirmWildcardBan: boolean; - features: string[]; - ban: { - defaultReasons: string[] - } - }; - protections: { - wordlist: { - words: string[]; - minutesBeforeTrusting: number; - }; - }; - health: { - healthz: { - enabled: boolean; - port: number; - address: string; - endpoint: string; - healthyStatus: number; - unhealthyStatus: number; - }; - // If specified, attempt to upload any crash statistics to sentry. - sentry?: { - dsn: string; - - // Frequency of performance monitoring. - // - // A number in [0.0, 1.0], where 0.0 means "don't bother with tracing" - // and 1.0 means "trace performance at every opportunity". - tracesSampleRate: number; - }; - }; - web: { - enabled: boolean; - port: number; - address: string; - abuseReporting: { - enabled: boolean; - } - }; - // Store room state using sqlite to improve startup time when Synapse responds - // slowly to requests for `/state`. - roomStateBackingStore: { - enabled?: boolean; - }; - // Experimental usage of the matrix-bot-sdk rust crypto. - // This can not be used with Pantalaimon. - experimentalRustCrypto: boolean; - - /** - * Config options only set at runtime. Try to avoid using the objects - * here as much as possible. - */ +export interface IConfig extends ConfigSchema { RUNTIME: { client?: MatrixClient; }; @@ -154,19 +60,14 @@ const defaultConfig: IConfig = { password: "", }, dataPath: "/data/storage", - acceptInvitesFromSpace: '!noop:example.org', + managementRoom: '!noop:example.org' as StringRoomID, + acceptInvitesFromSpace: '!noop:example.org' as StringRoomID, autojoinOnlyIfManager: true, recordIgnoredInvites: false, - managementRoom: "!noop:example.org", - verboseLogging: false, logLevel: "INFO", logMutedModules: ['MatrixHttpClient', 'MatrixClientLite'], - syncOnStartup: true, - verifyPermissionsOnStartup: true, noop: false, disableServerACL: false, - protectedRooms: [], - fasterMembershipChecks: false, automaticallyRedactForReasons: ["spam", "advertising"], protectAllJoinedRooms: false, backgroundDelayMS: 500, @@ -176,9 +77,6 @@ const defaultConfig: IConfig = { allowNoPrefix: false, additionalPrefixes: ["draupnir"], confirmWildcardBan: true, - features: [ - "synapse admin", - ], ban: { defaultReasons: [ "spam", @@ -203,6 +101,7 @@ const defaultConfig: IConfig = { healthyStatus: 200, unhealthyStatus: 418, }, + sentry: undefined, }, web: { enabled: false, @@ -229,19 +128,43 @@ export function getDefaultConfig(): IConfig { /** * @returns The users's raw config, deep copied over the `defaultConfig`. */ -function readConfigSource(): IConfig { +function readConfigSource(): Record { const explicitConfigPath = getCommandLineOption(process.argv, "--draupnir-config"); if (explicitConfigPath !== undefined) { const content = fs.readFileSync(explicitConfigPath, "utf8"); const parsed = load(content); return Config.util.extendDeep({}, defaultConfig, parsed); } else { - return Config.util.extendDeep({}, defaultConfig, Config.util.toObject()) as IConfig; + return Config.util.extendDeep({}, defaultConfig, Config.util.toObject()); } } +function readConfigSourceAndValidate(): ActionResult { + const source = readConfigSource(); + const decodedSource = Value.Decode(ConfigSchema, source, {suppressLogOnError: true}); + return decodedSource as ActionResult; +} + +function renderValidationError(error: ValueError): void { + log.error( + `Config value at path ${error.path} failed to validate with value:`, + error.value, + `and the following message:`, + error.message + ); + log.info(`Here is the description for ${error.path}:`, error.schema.description); +} + export function read(): IConfig { - const config = readConfigSource(); + const configResult = readConfigSourceAndValidate(); + if (isError(configResult)) { + configResult.error.errors.forEach(renderValidationError) + if (configResult.error.errors.length === 0) { + log.error(`Config error:`, configResult.error.exception); + } + throw new TypeError(`Config file failed validation`); + } + const config = configResult.ok; const explicitAccessTokenPath = getCommandLineOption(process.argv, "--access-token-path"); const explicitPantalaimonPasswordPath = getCommandLineOption(process.argv, "--pantalaimon-password-path"); if (explicitAccessTokenPath !== undefined) { diff --git a/src/protections/TrustedReporters.ts b/src/protections/TrustedReporters.ts index 25520fcb..53afe922 100644 --- a/src/protections/TrustedReporters.ts +++ b/src/protections/TrustedReporters.ts @@ -148,7 +148,7 @@ export class TrustedReporters extends AbstractProtection 0) { - await this.draupnir.client.sendMessage(this.draupnir.config.managementRoom, { + await this.draupnir.client.sendMessage(this.draupnir.managementRoomID, { msgtype: "m.notice", body: `message ${report.event_id} reported by ${[...reporters].join(', ')}. ` + `actions: ${met.join(', ')}` diff --git a/test/integration/commands/redactCommandTest.ts b/test/integration/commands/redactCommandTest.ts index cb151913..720c3ae3 100644 --- a/test/integration/commands/redactCommandTest.ts +++ b/test/integration/commands/redactCommandTest.ts @@ -25,7 +25,7 @@ describe("Test: The redaction command", function () { let mjolnirUserId = await mjolnir.getUserId(); let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - await moderator.joinRoom(this.config.managementRoom); + await moderator.joinRoom(this.draupnir!.managementRoomID); let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100); await badUser.joinRoom(targetRoom); @@ -76,7 +76,7 @@ describe("Test: The redaction command", function () { let mjolnirUserId = await draupnir.client.getUserId(); let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - await moderator.joinRoom(this.config.managementRoom); + await moderator.joinRoom(draupnir.managementRoomID); let targetRooms: string[] = []; for (let i = 0; i < 5; i++) { let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); @@ -123,7 +123,7 @@ describe("Test: The redaction command", function () { const draupnir = this.draupnir!; let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - await moderator.joinRoom(this.config.managementRoom); + await moderator.joinRoom(draupnir.managementRoomID); let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), draupnir.clientUserID]}); await moderator.setUserPowerLevel(draupnir.clientUserID, targetRoom, 100); await badUser.joinRoom(targetRoom); diff --git a/test/integration/commands/roomsTest.ts b/test/integration/commands/roomsTest.ts index a1cbf553..66276a27 100644 --- a/test/integration/commands/roomsTest.ts +++ b/test/integration/commands/roomsTest.ts @@ -19,7 +19,7 @@ describe("Test: The rooms commands", function () { const draupnir = this.draupnir!; let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - await moderator.joinRoom(this.config.managementRoom); + await moderator.joinRoom(draupnir.managementRoomID); let targetRoom = await moderator.createRoom({ invite: [draupnir.clientUserID]}); await moderator.setUserPowerLevel(draupnir.clientUserID, targetRoom, 100); diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index e8f5b570..6027a985 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -19,6 +19,9 @@ export const mochaHooks = { // Sometimes it takes a little longer to register users. this.timeout(30000); const config = this.config = configRead(); + if (config.managementRoom !== '#moderators:localhost:9999') { + throw new TypeError(`Test harness isn't built to use any other alias than #moderators:localhost:9999, sorry about it.`) + } this.managementRoomAlias = config.managementRoom; this.draupnir = await makeMjolnir(config); config.RUNTIME.client = draupnirClient()!; diff --git a/test/integration/helloTest.ts b/test/integration/helloTest.ts index cdc4425f..21ede7fb 100644 --- a/test/integration/helloTest.ts +++ b/test/integration/helloTest.ts @@ -14,7 +14,7 @@ describe("Test: !help command", function() { it('Mjolnir responded to !mjolnir help', async function(this: DraupnirTestContext) { this.timeout(30000); // send a messgage - await client.joinRoom(this.config.managementRoom); + await client.joinRoom(this.draupnir!.managementRoomID); // listener for getting the event reply let reply = new Promise((resolve, reject) => { client.on('room.message', noticeListener(this.draupnir!.managementRoomID, (event) => { diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index 527e0e61..adce2fc3 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -27,7 +27,7 @@ import { IConfig } from "../../src/config"; import { Draupnir } from "../../src/Draupnir"; import { makeDraupnirBotModeFromConfig } from "../../src/DraupnirBotMode"; import { SafeMatrixEmitterWrapper } from "matrix-protection-suite-for-matrix-bot-sdk"; -import { DefaultEventDecoder, RoomStateBackingStore } from "matrix-protection-suite"; +import { DefaultEventDecoder, RoomStateBackingStore, StringRoomAlias } from "matrix-protection-suite"; import { WebAPIs } from "../../src/webapis/WebAPIs"; patchMatrixClient(); @@ -98,7 +98,7 @@ export async function makeMjolnir(config: IConfig, backingStore?: RoomStateBacki const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()); const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password); await overrideRatelimitForUser(config.homeserverUrl, await client.getUserId()); - await ensureAliasedRoomExists(client, config.managementRoom); + await ensureAliasedRoomExists(client, config.managementRoom as StringRoomAlias); let mj = await makeDraupnirBotModeFromConfig(client, new SafeMatrixEmitterWrapper(client, DefaultEventDecoder), config, backingStore); globalClient = client; globalMjolnir = mj; diff --git a/test/integration/utilsTest.ts b/test/integration/utilsTest.ts index 0e7cbeaa..503cead2 100644 --- a/test/integration/utilsTest.ts +++ b/test/integration/utilsTest.ts @@ -1,10 +1,11 @@ import { strict as assert } from "assert"; import { LogLevel } from "matrix-bot-sdk"; import { DraupnirTestContext, draupnirClient } from "./mjolnirSetupUtils"; +import { StringRoomAlias } from "matrix-protection-suite"; describe("Test: utils", function() { it("replaceRoomIdsWithPills correctly turns a room ID in to a pill", async function(this: DraupnirTestContext) { - const managementRoomAlias = this.config.managementRoom; + const managementRoomAlias = this.config.managementRoom as StringRoomAlias; const draupnir = this.draupnir!; const managementRoomOutput = draupnir.managementRoomOutput; await draupnir.client.sendStateEvent(