diff --git a/package.json b/package.json index 1f5ee70..8b8c6ba 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts" }, "devDependencies": { + "@types/config": "0.0.41", "@types/crypto-js": "^4.0.2", "@types/jsdom": "^16.2.11", "@types/mocha": "^9.0.0", diff --git a/src/ErrorCache.ts b/src/ErrorCache.ts index bc66479..0c00387 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/Mjolnir.ts b/src/Mjolnir.ts index 3015b70..15fa160 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -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; diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 97bde07..1ac47e7 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -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 e0672e1..68f0786 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/UnbanBanCommand.ts b/src/commands/UnbanBanCommand.ts index 7aa00d4..c2f126c 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 43463ef..b23a54c 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 3f9cb3f..81fbf07 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 3acde9f..e26f481 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" }; + eventContent = [OutType.msg, ""]; } else if ("content" in event) { const MAX_EVENT_CONTENT_LENGTH = 2048; const MAX_NEWLINES = 64; if ("formatted_body" in event.content) { - eventContent = { html: this.limitLength(event.content.formatted_body, MAX_EVENT_CONTENT_LENGTH, MAX_NEWLINES) }; + eventContent = [OutType.html, this.limitLength(event.content.formatted_body, MAX_EVENT_CONTENT_LENGTH, MAX_NEWLINES)]; } else if ("body" in event.content) { - eventContent = { text: this.limitLength(event.content.body, MAX_EVENT_CONTENT_LENGTH, MAX_NEWLINES) }; + eventContent = [OutType.text, this.limitLength(event.content.body, MAX_EVENT_CONTENT_LENGTH, MAX_NEWLINES)]; } else { - eventContent = { text: this.limitLength(JSON.stringify(event["content"], null, 2), MAX_EVENT_CONTENT_LENGTH, MAX_NEWLINES) }; + eventContent = [OutType.text, this.limitLength(JSON.stringify(event["content"], null, 2), MAX_EVENT_CONTENT_LENGTH, MAX_NEWLINES)]; } + } else { + eventContent = [OutType.msg, "Malformed event, cannot read content."]; } } catch (ex) { - eventContent = { msg: `.` }; + eventContent = [OutType.msg, `.`]; } let accusedId = event["sender"]; 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,17 +838,18 @@ 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) { - if ("msg" in value) { - node.textContent = value.msg; - } else if ("text" in value) { - node.textContent = value.text; - } else if ("html" in value) { - node.innerHTML = value.html; + let [outType, out]: [OutType, string] = value; + if (outType === OutType.msg) { + node.textContent = out; + } else if (outType === OutType.text) { + node.textContent = out; + } else if (outType === OutType.html) { + node.innerHTML = out; } } } @@ -868,8 +875,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 64ebcc3..3f43ea2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -225,11 +225,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 +334,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 +344,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 ded0f3b..606ddc3 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 0e5ca49..79ca60a 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",