From f70d97e4d914f0441e2db8d4e92a1b8f24db2c17 Mon Sep 17 00:00:00 2001 From: Jess Porter Date: Wed, 2 Feb 2022 12:43:05 +0000 Subject: [PATCH] enable noImplicitAny (#209) --- package.json | 3 ++- src/ErrorCache.ts | 2 +- src/LogProxy.ts | 3 +-- src/Mjolnir.ts | 31 +++++++++++++-------------- src/commands/AliasCommands.ts | 2 +- src/commands/CommandHandler.ts | 4 ++-- src/commands/CreateBanListCommand.ts | 7 +++--- src/commands/DumpRulesCommand.ts | 2 +- src/commands/ProtectionsCommands.ts | 2 +- src/commands/UnbanBanCommand.ts | 4 ++-- src/models/BanList.ts | 2 +- src/protections/BasicFlooding.ts | 15 ++++++------- src/protections/ProtectionSettings.ts | 12 +++++------ src/report/ReportManager.ts | 22 ++++++++++--------- src/utils.ts | 22 ++++++++++++++----- src/webapis/WebAPIs.ts | 7 ++++-- tsconfig.json | 2 +- 17 files changed, 78 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index abbbdcff..624dfd2d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts" }, "devDependencies": { + "@types/config": "0.0.41", "@types/crypto-js": "^4.0.2", + "@types/html-to-text": "^8.0.1", "@types/jsdom": "^16.2.11", "@types/mocha": "^9.0.0", "@types/node": "^16.7.10", @@ -31,7 +33,6 @@ }, "dependencies": { "config": "^3.3.6", - "escape-html": "^1.0.3", "express": "^4.17", "html-to-text": "^8.0.0", "js-yaml": "^4.1.0", diff --git a/src/ErrorCache.ts b/src/ErrorCache.ts index bc664797..0c003875 100644 --- a/src/ErrorCache.ts +++ b/src/ErrorCache.ts @@ -17,7 +17,7 @@ limitations under the License. export const ERROR_KIND_PERMISSION = "permission"; export const ERROR_KIND_FATAL = "fatal"; -const TRIGGER_INTERVALS = { +const TRIGGER_INTERVALS: { [key: string]: number } = { [ERROR_KIND_PERMISSION]: 3 * 60 * 60 * 1000, // 3 hours [ERROR_KIND_FATAL]: 15 * 60 * 1000, // 15 minutes }; diff --git a/src/LogProxy.ts b/src/LogProxy.ts index 417ce8ec..ab2d807f 100644 --- a/src/LogProxy.ts +++ b/src/LogProxy.ts @@ -16,8 +16,7 @@ limitations under the License. import { LogLevel, LogService, TextualMessageEventContent } from "matrix-bot-sdk"; import config from "./config"; -import { replaceRoomIdsWithPills } from "./utils"; -import * as htmlEscape from "escape-html"; +import { htmlEscape, replaceRoomIdsWithPills } from "./utils"; const levelToFn = { [LogLevel.DEBUG.toString()]: LogService.debug, diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 3015b702..a7a901fd 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -40,7 +40,7 @@ import { ProtectionSettingValidationError } from "./protections/ProtectionSettin import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue"; import { Healthz } from "./health/healthz"; import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue"; -import * as htmlEscape from "escape-html"; +import { htmlEscape } from "./utils"; import { ReportManager } from "./report/ReportManager"; import { WebAPIs } from "./webapis/WebAPIs"; import RuleServer from "./models/RuleServer"; @@ -84,7 +84,7 @@ export class Mjolnir { * @param {boolean} options.autojoinOnlyIfManager Whether to only accept an invitation by a user present in the `managementRoom`. * @param {string} options.acceptInvitesFromGroup A group of users to accept invites from, ignores invites form users not in this group. */ - private static addJoinOnInviteListener(mjolnir: Mjolnir, client: MatrixClient, options) { + private static addJoinOnInviteListener(mjolnir: Mjolnir, client: MatrixClient, options: { [key: string]: any }) { client.on("room.invite", async (roomId: string, inviteEvent: any) => { const membershipEvent = new MembershipEvent(inviteEvent); @@ -271,7 +271,7 @@ export class Mjolnir { await logMessage(LogLevel.DEBUG, "Mjolnir@startup", "Loading protected rooms..."); await this.resyncJoinedRooms(false); try { - const data: Object | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE); + const data: { rooms?: string[] } | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE); if (data && data['rooms']) { for (const roomId of data['rooms']) { this.protectedRooms[roomId] = Permalinks.forRoom(roomId); @@ -328,15 +328,15 @@ export class Mjolnir { if (unprotectedIdx >= 0) this.knownUnprotectedRooms.splice(unprotectedIdx, 1); this.explicitlyProtectedRoomIds.push(roomId); - let additionalProtectedRooms; + let additionalProtectedRooms: { rooms?: string[] } | null = null; try { additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE); } catch (e) { LogService.warn("Mjolnir", extractRequestError(e)); } - if (!additionalProtectedRooms || !additionalProtectedRooms['rooms']) additionalProtectedRooms = { rooms: [] }; - additionalProtectedRooms.rooms.push(roomId); - await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, additionalProtectedRooms); + const rooms = (additionalProtectedRooms?.rooms ?? []); + rooms.push(roomId); + await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms }); await this.syncLists(config.verboseLogging); } @@ -346,14 +346,13 @@ export class Mjolnir { const idx = this.explicitlyProtectedRoomIds.indexOf(roomId); if (idx >= 0) this.explicitlyProtectedRoomIds.splice(idx, 1); - let additionalProtectedRooms; + let additionalProtectedRooms: { rooms?: string[] } | null = null; try { additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE); } catch (e) { LogService.warn("Mjolnir", extractRequestError(e)); } - if (!additionalProtectedRooms || !additionalProtectedRooms['rooms']) additionalProtectedRooms = { rooms: [] }; - additionalProtectedRooms.rooms = additionalProtectedRooms.rooms.filter(r => r !== roomId); + additionalProtectedRooms = { rooms: additionalProtectedRooms?.rooms?.filter(r => r !== roomId) ?? [] }; await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, additionalProtectedRooms); } @@ -379,7 +378,7 @@ export class Mjolnir { private async getEnabledProtections() { let enabled: string[] = []; try { - const protections: Object | null = await this.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE); + const protections: { enabled: string[] } | null = await this.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE); if (protections && protections['enabled']) { for (const protection of protections['enabled']) { enabled.push(protection); @@ -555,8 +554,8 @@ export class Mjolnir { this.applyUnprotectedRooms(); try { - const accountData: Object | null = await this.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId); - if (accountData && accountData['warned']) return; // already warned + const accountData: { warned: boolean } | null = await this.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId); + if (accountData && accountData.warned) return; // already warned } catch (e) { // Ignore - probably haven't warned about it yet } @@ -575,14 +574,14 @@ export class Mjolnir { const banLists: BanList[] = []; const joinedRooms = await this.client.getJoinedRooms(); - let watchedListsEvent = {}; + let watchedListsEvent: { references?: string[] } | null = null; try { watchedListsEvent = await this.client.getAccountData(WATCHED_LISTS_EVENT_TYPE); } catch (e) { // ignore - not important } - for (const roomRef of (watchedListsEvent['references'] || [])) { + for (const roomRef of (watchedListsEvent?.references || [])) { const permalink = Permalinks.parseUrl(roomRef); if (!permalink.roomIdOrAlias) continue; @@ -852,7 +851,7 @@ export class Mjolnir { } else if (ruleKind === RULE_ROOM) { ruleKind = 'room'; } - html += `
  • ${change.changeType} ${htmlEscape(ruleKind)} (${htmlEscape(rule.recommendation)}): ${htmlEscape(rule.entity)} (${htmlEscape(rule.reason)})
  • `; + html += `
  • ${change.changeType} ${htmlEscape(ruleKind)} (${htmlEscape(rule.recommendation ?? "")}): ${htmlEscape(rule.entity)} (${htmlEscape(rule.reason)})
  • `; text += `* ${change.changeType} ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`; } diff --git a/src/commands/AliasCommands.ts b/src/commands/AliasCommands.ts index 2539fab6..59ff4ce0 100644 --- a/src/commands/AliasCommands.ts +++ b/src/commands/AliasCommands.ts @@ -16,7 +16,7 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; -import * as htmlEscape from "escape-html"; +import { htmlEscape } from "../utils"; // !mjolnir move export async function execMoveAliasCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 97bde076..d1389583 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -19,7 +19,7 @@ import { execStatusCommand } from "./StatusCommand"; import { execBanCommand, execUnbanCommand } from "./UnbanBanCommand"; import { execDumpRulesCommand } from "./DumpRulesCommand"; import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk"; -import * as htmlEscape from "escape-html"; +import { htmlEscape } from "../utils"; import { execSyncCommand } from "./SyncCommand"; import { execPermissionCheckCommand } from "./PermissionCheckCommand"; import { execCreateListCommand } from "./CreateBanListCommand"; @@ -40,7 +40,7 @@ import { execKickCommand } from "./KickCommand"; export const COMMAND_PREFIX = "!mjolnir"; -export async function handleCommand(roomId: string, event: any, mjolnir: Mjolnir) { +export async function handleCommand(roomId: string, event: { content: { body: string } }, mjolnir: Mjolnir) { const cmd = event['content']['body']; const parts = cmd.trim().split(' ').filter(p => p.trim().length > 0); diff --git a/src/commands/CreateBanListCommand.ts b/src/commands/CreateBanListCommand.ts index e0672e1e..68f07864 100644 --- a/src/commands/CreateBanListCommand.ts +++ b/src/commands/CreateBanListCommand.ts @@ -23,7 +23,7 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir: const shortcode = parts[3]; const aliasLocalpart = parts[4]; - const powerLevels = { + const powerLevels: { [key: string]: any } = { "ban": 50, "events": { "m.room.name": 100, @@ -38,12 +38,11 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir: "redact": 50, "state_default": 50, "users": { - // populated in a moment + [await mjolnir.client.getUserId()]: 100, + [event["sender"]]: 50 }, "users_default": 0, }; - powerLevels['users'][await mjolnir.client.getUserId()] = 100; - powerLevels['users'][event['sender']] = 50; const listRoomId = await mjolnir.client.createRoom({ preset: "public_chat", diff --git a/src/commands/DumpRulesCommand.ts b/src/commands/DumpRulesCommand.ts index a904546c..a2ee1426 100644 --- a/src/commands/DumpRulesCommand.ts +++ b/src/commands/DumpRulesCommand.ts @@ -16,7 +16,7 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; -import * as htmlEscape from "escape-html"; +import { htmlEscape } from "../utils"; // !mjolnir rules export async function execDumpRulesCommand(roomId: string, event: any, mjolnir: Mjolnir) { diff --git a/src/commands/ProtectionsCommands.ts b/src/commands/ProtectionsCommands.ts index 601f2fb0..83207589 100644 --- a/src/commands/ProtectionsCommands.ts +++ b/src/commands/ProtectionsCommands.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as htmlEscape from "escape-html"; +import { htmlEscape } from "../utils"; import { Mjolnir } from "../Mjolnir"; import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk"; import { PROTECTIONS } from "../protections/protections"; diff --git a/src/commands/UnbanBanCommand.ts b/src/commands/UnbanBanCommand.ts index 7aa00d49..c2f126c3 100644 --- a/src/commands/UnbanBanCommand.ts +++ b/src/commands/UnbanBanCommand.ts @@ -31,9 +31,9 @@ interface Arguments { // Exported for tests export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise { - let defaultShortcode = null; + let defaultShortcode: string | null = null; try { - const data: Object = await mjolnir.client.getAccountData(DEFAULT_LIST_EVENT_TYPE); + const data: { shortcode: string } = await mjolnir.client.getAccountData(DEFAULT_LIST_EVENT_TYPE); defaultShortcode = data['shortcode']; } catch (e) { LogService.warn("UnbanBanCommand", "Non-fatal error getting default ban list"); diff --git a/src/models/BanList.ts b/src/models/BanList.ts index 43463ef4..b23a54cc 100644 --- a/src/models/BanList.ts +++ b/src/models/BanList.ts @@ -91,7 +91,7 @@ class BanList extends EventEmitter { * @param roomRef A sharable/clickable matrix URL that refers to the room. * @param client A matrix client that is used to read the state of the room when `updateList` is called. */ - constructor(public readonly roomId: string, public readonly roomRef, private client: MatrixClient) { + constructor(public readonly roomId: string, public readonly roomRef: string, private client: MatrixClient) { super(); } diff --git a/src/protections/BasicFlooding.ts b/src/protections/BasicFlooding.ts index 3f9cb3f4..81fbf07f 100644 --- a/src/protections/BasicFlooding.ts +++ b/src/protections/BasicFlooding.ts @@ -30,12 +30,11 @@ export class BasicFlooding implements IProtection { private lastEvents: { [roomId: string]: { [userId: string]: { originServerTs: number, eventId: string }[] } } = {}; private recentlyBanned: string[] = []; - maxPerMinute = new NumberProtectionSetting(DEFAULT_MAX_PER_MINUTE); - settings = {}; + settings = { + maxPerMinute: new NumberProtectionSetting(DEFAULT_MAX_PER_MINUTE) + }; - constructor() { - this.settings['maxPerMinute'] = this.maxPerMinute; - } + constructor() { } public get name(): string { return 'BasicFloodingProtection'; @@ -62,7 +61,7 @@ export class BasicFlooding implements IProtection { messageCount++; } - if (messageCount >= this.maxPerMinute.value) { + if (messageCount >= this.settings.maxPerMinute.value) { await logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId); if (!config.noop) { await mjolnir.client.banUser(event['sender'], roomId, "spam"); @@ -88,8 +87,8 @@ export class BasicFlooding implements IProtection { } // Trim the oldest messages off the user's history if it's getting large - if (forUser.length > this.maxPerMinute.value * 2) { - forUser.splice(0, forUser.length - (this.maxPerMinute.value * 2) - 1); + if (forUser.length > this.settings.maxPerMinute.value * 2) { + forUser.splice(0, forUser.length - (this.settings.maxPerMinute.value * 2) - 1); } } } diff --git a/src/protections/ProtectionSettings.ts b/src/protections/ProtectionSettings.ts index 3acde9f2..e26f4815 100644 --- a/src/protections/ProtectionSettings.ts +++ b/src/protections/ProtectionSettings.ts @@ -76,13 +76,13 @@ export function isListSetting(object: any): object is AbstractProtectionListSett export class StringProtectionSetting extends AbstractProtectionSetting { value = ""; - fromString = (data) => data; - validate = (data) => true; + fromString = (data: string): string => data; + validate = (data: string): boolean => true; } export class StringListProtectionSetting extends AbstractProtectionListSetting { value: string[] = []; - fromString = (data) => data; - validate = (data) => true; + fromString = (data: string): string => data; + validate = (data: string): boolean => true; addValue(data: string): string[] { return [...this.value, data]; } @@ -107,11 +107,11 @@ export class NumberProtectionSetting extends AbstractProtectionSetting" }; @@ -707,6 +707,8 @@ class DisplayManager { } else { eventContent = { text: this.limitLength(JSON.stringify(event["content"], null, 2), MAX_EVENT_CONTENT_LENGTH, MAX_NEWLINES) }; } + } else { + eventContent = { msg: "Malformed event, cannot read content." }; } } catch (ex) { eventContent = { msg: `.` }; @@ -716,12 +718,12 @@ class DisplayManager { let reporterDisplayName: string, accusedDisplayName: string; try { - reporterDisplayName = await this.owner.mjolnir.client.getUserProfile(reporterId)["displayname"] || reporterId; + reporterDisplayName = (await this.owner.mjolnir.client.getUserProfile(reporterId))["displayname"] || reporterId; } catch (ex) { reporterDisplayName = ""; } try { - accusedDisplayName = await this.owner.mjolnir.client.getUserProfile(accusedId)["displayname"] || accusedId; + accusedDisplayName = (await this.owner.mjolnir.client.getUserProfile(accusedId))["displayname"] || accusedId; } catch (ex) { accusedDisplayName = ""; } @@ -832,8 +834,8 @@ class DisplayManager { } // ...insert HTML content - for (let [key, value] of [ - ['event-content', eventContent], + for (let {key, value} of [ + { key: 'event-content', value: eventContent }, ]) { let node = document.getElementById(key); if (node) { @@ -842,7 +844,7 @@ class DisplayManager { } else if ("text" in value) { node.textContent = value.text; } else if ("html" in value) { - node.innerHTML = value.html; + node.innerHTML = value.html } } } @@ -868,8 +870,8 @@ class DisplayManager { body: htmlToText(document.body.outerHTML, { wordwrap: false }), format: "org.matrix.custom.html", formatted_body: document.body.outerHTML, + [ABUSE_REPORT_KEY]: report }; - notice[ABUSE_REPORT_KEY] = report; let noticeEventId = await this.owner.mjolnir.client.sendMessage(this.owner.mjolnir.managementRoomId, notice); if (kind !== Kind.ERROR) { diff --git a/src/utils.ts b/src/utils.ts index 64ebcc35..29988948 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -29,9 +29,17 @@ import { } from "matrix-bot-sdk"; import { logMessage } from "./LogProxy"; import config from "./config"; -import * as htmlEscape from "escape-html"; import { ClientRequest, IncomingMessage } from "http"; +export function htmlEscape(input: string): string { + return input.replace(/["&<>]/g, (char: string) => ({ + ['"'.charCodeAt(0)]: """, + ["&".charCodeAt(0)]: "&", + ["<".charCodeAt(0)]: "<", + [">".charCodeAt(0)]: ">" + })[char.charCodeAt(0)]); +} + export function setToArray(set: Set): T[] { const arr: T[] = []; for (const v of set) { @@ -225,11 +233,13 @@ function patchMatrixClientForConciseExceptions() { return; } let originalRequestFn = getRequestFn(); - setRequestFn((params, cb) => { + setRequestFn((params: { [k: string]: any }, cb: any) => { // Store an error early, to maintain *some* semblance of stack. // We'll only throw the error if there is one. let error = new Error("STACK CAPTURE"); - originalRequestFn(params, function conciseExceptionRequestFn(err, response, resBody) { + originalRequestFn(params, function conciseExceptionRequestFn( + err: { [key: string]: any }, response: { [key: string]: any }, resBody: string + ) { if (!err && (response?.statusCode < 200 || response?.statusCode >= 300)) { // Normally, converting HTTP Errors into rejections is done by the caller // of `requestFn` within matrix-bot-sdk. However, this always ends up rejecting @@ -332,7 +342,7 @@ function patchMatrixClientForRetry() { return; } let originalRequestFn = getRequestFn(); - setRequestFn(async (params, cb) => { + setRequestFn(async (params: { [k: string]: any }, cb: any) => { let attempt = 1; numberOfConcurrentRequests += 1; if (TRACE_CONCURRENT_REQUESTS) { @@ -342,7 +352,9 @@ function patchMatrixClientForRetry() { while (true) { try { let result: any[] = await new Promise((resolve, reject) => { - originalRequestFn(params, function requestFnWithRetry(err, response, resBody) { + originalRequestFn(params, function requestFnWithRetry( + err: { [key: string]: any }, response: { [key: string]: any }, resBody: string + ) { // Note: There is no data race on `attempt` as we `await` before continuing // to the next iteration of the loop. if (attempt < MAX_REQUEST_ATTEMPTS && err?.body?.errcode === 'M_LIMIT_EXCEEDED') { diff --git a/src/webapis/WebAPIs.ts b/src/webapis/WebAPIs.ts index 7c0dc856..6e866e18 100644 --- a/src/webapis/WebAPIs.ts +++ b/src/webapis/WebAPIs.ts @@ -107,16 +107,19 @@ export class WebAPIs { { // -- Create a client on behalf of the reporter. // We'll use it to confirm the authenticity of the report. - let accessToken; + let accessToken: string | undefined = undefined; // Authentication mechanism 1: Request header. let authorization = request.get('Authorization'); if (authorization) { [, accessToken] = AUTHORIZATION.exec(authorization)!; - } else { + } else if (typeof(request.query["access_token"]) === 'string') { // Authentication mechanism 2: Access token as query parameter. accessToken = request.query["access_token"]; + } else { + response.status(401).send("Missing access token"); + return; } // Create a client dedicated to this report. diff --git a/tsconfig.json b/tsconfig.json index 0e5ca495..79ca60af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "noImplicitReturns": true, "noUnusedLocals": true, "target": "es2015", - "noImplicitAny": false, + "noImplicitAny": true, "sourceMap": true, "strictNullChecks": true, "outDir": "./lib",