/* Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { MatrixClient } from "matrix-bot-sdk"; import BanList, { ALL_RULE_TYPES } from "./models/BanList"; import { applyServerAcls } from "./actions/ApplyAcl"; import { RoomUpdateError } from "./models/RoomUpdateError"; import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; import { applyUserBans } from "./actions/ApplyBan"; export class Mjolnir { private displayName: string; private localpart: string; constructor( public readonly client: MatrixClient, public readonly managementRoomId: string, public readonly publishedBanListRoomId: string, public readonly protectedRooms: { [roomId: string]: string }, public readonly banLists: BanList[], ) { client.on("room.event", this.handleEvent.bind(this)); client.on("room.message", async (roomId, event) => { if (roomId !== managementRoomId) return; if (!event['content']) return; const content = event['content']; if (content['msgtype'] === "m.text" && content['body']) { const prefixes = [COMMAND_PREFIX, this.localpart + ":", this.displayName + ":", await client.getUserId() + ":"]; const prefixUsed = prefixes.find(p => content['body'].startsWith(p)); if (!prefixUsed) return; // rewrite the event body to make the prefix uniform (in case the bot has spaces in its display name) event['content']['body'] = COMMAND_PREFIX + content['body'].substring(prefixUsed.length); await client.sendReadReceipt(roomId, event['event_id']); return handleCommand(roomId, event, this); } }); client.getUserId().then(userId => { this.localpart = userId.split(':')[0].substring(1); return client.getUserProfile(userId); }).then(profile => { if (profile['displayname']) { this.displayName = profile['displayname']; } }) } public start() { return this.client.start(); } public async syncLists() { for (const list of this.banLists) { await list.updateList(); } let hadErrors = false; const aclErrors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this); const banErrors = await applyUserBans(this.banLists, Object.keys(this.protectedRooms), this); hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:"); hadErrors = hadErrors || await this.printActionResult(banErrors, "Errors updating member bans:"); if (!hadErrors) { const html = `Done updating rooms - no errors`; const text = "Updated all protected rooms with new rules successfully"; await this.client.sendMessage(this.managementRoomId, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", formatted_body: html, }); } } public async syncListForRoom(roomId: string) { let updated = false; for (const list of this.banLists) { if (list.roomId !== roomId) continue; await list.updateList(); updated = true; } if (!updated) return; let hadErrors = false; const aclErrors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this); const banErrors = await applyUserBans(this.banLists, Object.keys(this.protectedRooms), this); hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:"); hadErrors = hadErrors || await this.printActionResult(banErrors, "Errors updating member bans:"); if (!hadErrors) { const html = `Done updating rooms - no errors`; const text = "Done updating rooms - no errors"; await this.client.sendMessage(this.managementRoomId, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", formatted_body: html, }); } } private async handleEvent(roomId: string, event: any) { if (!event['state_key']) return; // we also don't do anything with state events that have no state key if (ALL_RULE_TYPES.includes(event['type'])) { await this.syncListForRoom(roomId); } else if (event['type'] === "m.room.member") { const errors = await applyUserBans(this.banLists, Object.keys(this.protectedRooms), this); const hadErrors = await this.printActionResult(errors); if (!hadErrors) { const html = `Done updating rooms - no errors`; const text = "Done updating rooms - no errors"; await this.client.sendMessage(this.managementRoomId, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", formatted_body: html, }); } } else return; // Not processed } private async printActionResult(errors: RoomUpdateError[], title: string=null) { if (errors.length <= 0) return false; let html = ""; let text = ""; const htmlTitle = title ? `${title}
` : ''; const textTitle = title ? `${title}\n` : ''; html += `${htmlTitle}${errors.length} errors updating protected rooms!
"; const message = { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", formatted_body: html, }; await this.client.sendMessage(this.managementRoomId, message); return true; } }