diff --git a/README.md b/README.md index e4677f2..26fb086 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Phase 2: * [ ] Redact messages on ban (optionally) * [x] More useful spam in management room * [ ] Command to import ACLs, etc from rooms -* [ ] Vet rooms on startup option +* [x] Vet rooms on startup option * [ ] Command to actually unban users (instead of leaving them stuck) Phase 3: diff --git a/config/default.yaml b/config/default.yaml index 3a567ce..de0fbbd 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -37,6 +37,11 @@ verboseLogging: true # is the same as running !mjolnir sync immediately after startup. syncOnStartup: true +# Set to false to prevent Mjolnir from checking its permissions on startup. This +# is recommended to be left as "true" to catch room permission problems (state +# resets, etc) before Mjolnir is needed. +verifyPermissionsOnStartup: true + # The room ID or alias where the bot's own personal ban list is kept. This is # where the commands to manage a ban list end up being routed to. Note that # this room is NOT automatically added to the banLists list below - you will diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 7c2e34f..1a8ba9a 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "matrix-bot-sdk"; +import { LogService, MatrixClient } from "matrix-bot-sdk"; import BanList, { ALL_RULE_TYPES } from "./models/BanList"; import { applyServerAcls } from "./actions/ApplyAcl"; import { RoomUpdateError } from "./models/RoomUpdateError"; @@ -75,14 +75,91 @@ export class Mjolnir { } public start() { - return this.client.start().then(() => { - if (config.syncOnStartup) { - this.client.sendNotice(this.managementRoomId, "Syncing lists..."); - return this.syncLists(); + return this.client.start().then(async () => { + this.currentState = STATE_CHECKING_PERMISSIONS; + if (config.verifyPermissionsOnStartup) { + await this.client.sendNotice(this.managementRoomId, "Checking permissions..."); + await this.verifyPermissions(); } + }).then(async () => { + this.currentState = STATE_SYNCING; + if (config.syncOnStartup) { + await this.client.sendNotice(this.managementRoomId, "Syncing lists..."); + await this.syncLists(); + } + }).then(async () => { + this.currentState = STATE_RUNNING; + await this.client.sendNotice(this.managementRoomId, "Startup complete."); }); } + public async verifyPermissions() { + const ownUserId = await this.client.getUserId(); + + const errors: RoomUpdateError[] = []; + for (const roomId of Object.keys(this.protectedRooms)) { + try { + const powerLevels = await this.client.getRoomStateEvent(roomId, "m.room.power_levels", ""); + if (!powerLevels) { + // noinspection ExceptionCaughtLocallyJS + throw new Error("Missing power levels state event"); + } + + function plDefault(val: number|undefined|null, def: number): number { + if (!val && val !== 0) return def; + return val; + } + + const users = powerLevels['users'] || {}; + const events = powerLevels['events'] || {}; + const usersDefault = plDefault(powerLevels['users_default'], 0); + const stateDefault = plDefault(powerLevels['state_default'], 50); + const ban = plDefault(powerLevels['ban'], 50); + const kick = plDefault(powerLevels['kick'], 50); + const redact = plDefault(powerLevels['redact'], 50); + + const userLevel = plDefault(users[ownUserId], usersDefault); + const aclLevel = plDefault(events["m.room.server_acl"], stateDefault); + + // Wants: ban, kick, redact, m.room.server_acl + + if (userLevel < ban) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(`Missing power level for bans: ${userLevel} < ${ban}`); + } + if (userLevel < kick) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(`Missing power level for kicks: ${userLevel} < ${kick}`); + } + if (userLevel < redact) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(`Missing power level for redactions: ${userLevel} < ${redact}`); + } + if (userLevel < aclLevel) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(`Missing power level for server ACLs: ${userLevel} < ${aclLevel}`); + } + + // Otherwise OK + } catch (e) { + LogService.error("Mjolnir", e); + errors.push({roomId, errorMessage: e.message || (e.body ? e.body.error : '')}); + } + } + + const hadErrors = await this.printActionResult(errors, "Permission errors in protected rooms:"); + if (!hadErrors) { + const html = `All permissions look OK.`; + const text = "All permissions look OK."; + await this.client.sendMessage(this.managementRoomId, { + msgtype: "m.notice", + body: text, + format: "org.matrix.custom.html", + formatted_body: html, + }); + } + } + public async syncLists() { for (const list of this.banLists) { await list.updateList(); diff --git a/src/config.ts b/src/config.ts index 4d7d44c..4d11170 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,6 +29,7 @@ interface IConfig { managementRoom: string; verboseLogging: boolean; syncOnStartup: boolean; + verifyPermissionsOnStartup: boolean; publishedBanListRoom: string; protectedRooms: string[]; // matrix.to urls banLists: string[]; // matrix.to urls diff --git a/src/index.ts b/src/index.ts index 3e23cca..6db3af5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -90,7 +90,6 @@ LogService.setLogger(new RichConsoleLogger()); const bot = new Mjolnir(client, managementRoomId, banListRoomId, protectedRooms, banLists); await bot.start(); - // TODO: Check permissions for mjolnir in protected rooms // TODO: Complain about permission changes in protected rooms (including after power levels change) LogService.info("index", "Bot started!")