From cff19081960cec32d3109f9f7ba25ad7ef95514d Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 17 Jul 2024 13:55:31 +0100 Subject: [PATCH] Update SuperCoolStream for eslint and ts5. --- src/Draupnir.ts | 4 +- src/ManagementRoomOutput.ts | 4 +- .../bot/AppserviceCommandHandler.ts | 4 +- src/appservice/bot/ListCommand.tsx | 6 +- src/appservice/postgres/PgDataStore.ts | 1 + .../interface-manager/CommandReader.ts | 128 ++++++++++++------ .../interface-manager/DeadDocument.ts | 7 +- .../interface-manager/ParameterParsing.ts | 22 +-- src/config.ts | 4 +- 9 files changed, 114 insertions(+), 66 deletions(-) diff --git a/src/Draupnir.ts b/src/Draupnir.ts index d81ea973..0aee4967 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -236,7 +236,7 @@ export class Draupnir implements Client { this.protectedRoomsSet.handleExternalInvite(roomID, event); } this.managementRoomMessageListener(roomID, event); - this.reactionHandler.handleEvent(roomID, event); + void Task((async () => { await this.reactionHandler.handleEvent(roomID, event); })()); if (this.protectedRoomsSet.isProtectedRoom(roomID)) { this.protectedRoomsSet.handleTimelineEvent(roomID, event); } @@ -248,7 +248,7 @@ export class Draupnir implements Client { } if (Value.Check(RoomMessage, event) && Value.Check(TextMessageContent, event.content)) { if (event.content.body === "** Unable to decrypt: The sender's device has not sent us the keys for this message. **") { - log.info(`Unable to decrypt an event ${event.event_id} from ${event.sender} in the management room ${this.managementRoom}.`); + log.info(`Unable to decrypt an event ${event.event_id} from ${event.sender} in the management room ${this.managementRoom.toPermalink()}.`); void Task(this.client.unstableApis.addReactionToEvent(roomID, event.event_id, '⚠').then(_ => Ok(undefined))); void Task(this.client.unstableApis.addReactionToEvent(roomID, event.event_id, 'UISI').then(_ => Ok(undefined))); void Task(this.client.unstableApis.addReactionToEvent(roomID, event.event_id, '🚨').then(_ => Ok(undefined))); diff --git a/src/ManagementRoomOutput.ts b/src/ManagementRoomOutput.ts index a8abcb55..ef7fbe5f 100644 --- a/src/ManagementRoomOutput.ts +++ b/src/ManagementRoomOutput.ts @@ -75,7 +75,7 @@ export default class ManagementRoomOutput { // Though spec doesn't say so, room ids that have slashes in them are accepted by Synapse and Dendrite unfortunately for us. const escapeRegex = (v: string): string => { - return v.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + return v.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); }; const viaServers = [serverName(this.clientUserID)]; @@ -108,7 +108,7 @@ export default class ManagementRoomOutput { * @param additionalRoomIds The roomIds in the message that we want to be replaced by room pills. * @param isRecursive Whether logMessage is being called from logMessage. */ - public async logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string | null = null, isRecursive = false): Promise { + public async logMessage(level: LogLevel, module: string, message: string, additionalRoomIds: string[] | string | null = null, isRecursive = false): Promise { if (level === LogLevel.ERROR) { Sentry.captureMessage(`${module}: ${message}`, 'error'); } diff --git a/src/appservice/bot/AppserviceCommandHandler.ts b/src/appservice/bot/AppserviceCommandHandler.ts index 2a4be172..ff5f1677 100644 --- a/src/appservice/bot/AppserviceCommandHandler.ts +++ b/src/appservice/bot/AppserviceCommandHandler.ts @@ -10,7 +10,7 @@ import { defineMatrixInterfaceAdaptor, findMatrixInterfaceAdaptor, MatrixContext import { ArgumentStream, RestDescription, findPresentationType, parameters } from '../../commands/interface-manager/ParameterParsing'; import { MjolnirAppService } from '../AppService'; import { renderHelp } from '../../commands/interface-manager/MatrixHelpRenderer'; -import { ActionResult, ClientPlatform, Ok, RoomMessage, StringRoomID, StringUserID, Value, isError } from 'matrix-protection-suite'; +import { ActionResult, ClientPlatform, Ok, RoomMessage, StringRoomID, StringUserID, Task, Value, isError } from 'matrix-protection-suite'; import { MatrixSendClient } from 'matrix-protection-suite-for-matrix-bot-sdk'; import { MatrixReactionHandler } from '../../commands/interface-manager/MatrixReactionHandler'; import { ARGUMENT_PROMPT_LISTENER, DEFAUILT_ARGUMENT_PROMPT_LISTENER, makeListenerForArgumentPrompt, makeListenerForPromptDefault } from '../../commands/interface-manager/MatrixPromptForAccept'; @@ -105,7 +105,7 @@ export class AppserviceCommandHandler { ...this.commandContext, event: parsedEvent, }; - adaptor.invoke(context, context, ...argumentStream.rest()); + void Task(adaptor.invoke(context, context, ...argumentStream.rest())); return; } } diff --git a/src/appservice/bot/ListCommand.tsx b/src/appservice/bot/ListCommand.tsx index 70bdd021..1efa38f9 100644 --- a/src/appservice/bot/ListCommand.tsx +++ b/src/appservice/bot/ListCommand.tsx @@ -10,7 +10,7 @@ import { AppserviceBaseExecutor } from './AppserviceCommandHandler'; import { tickCrossRenderer } from '../../commands/interface-manager/MatrixHelpRenderer'; import { DeadDocumentJSX } from '../../commands/interface-manager/JSXFactory'; import { renderMatrixAndSend } from '../../commands/interface-manager/DeadDocumentMatrix'; -import { ActionError, ActionResult, isError, Ok, UserID } from 'matrix-protection-suite'; +import { ActionError, ActionResult, isError, Ok, RoomEvent, UserID } from 'matrix-protection-suite'; import { MatrixSendClient } from 'matrix-protection-suite-for-matrix-bot-sdk'; import { UnstartedDraupnir } from '../../draupnirfactory/StandardDraupnirManager'; @@ -37,7 +37,7 @@ const listUnstarted = defineInterfaceCommand({ // and be used similar to like #=1 and #1. defineMatrixInterfaceAdaptor({ interfaceCommand: listUnstarted, - renderer: async function (this: MatrixInterfaceAdaptor, client: MatrixSendClient, commandRoomId: string, event: any, result: ActionResult) { + renderer: async function (this: MatrixInterfaceAdaptor, client: MatrixSendClient, commandRoomId: string, event: RoomEvent, result: ActionResult) { tickCrossRenderer.call(this, client, commandRoomId, event, result); // don't await, it doesn't really matter. if (isError(result)) { return; // just let the default handler deal with it. @@ -82,7 +82,7 @@ const restart = defineInterfaceCommand({ const draupnirManager = this.appservice.draupnirManager; const draupnir = draupnirManager.findUnstartedDraupnir(draupnirUser.toString()); if (draupnir !== undefined) { - return ActionError.Result(`We can't find the unstarted draupnir ${draupnirUser}, is it already running?`); + return ActionError.Result(`We can't find the unstarted draupnir ${draupnirUser.toString()}, is it already running?`); } return await draupnirManager.startDraupnirFromMXID(draupnirUser.toString()); }, diff --git a/src/appservice/postgres/PgDataStore.ts b/src/appservice/postgres/PgDataStore.ts index 7fbfab54..2b33c797 100644 --- a/src/appservice/postgres/PgDataStore.ts +++ b/src/appservice/postgres/PgDataStore.ts @@ -21,6 +21,7 @@ function getSchema(): SchemaUpdateFunction[] { const nSchema = 2; const schema = []; for (let schemaID = 1; schemaID < nSchema + 1; schemaID++) { + // eslint-disable-next-line @typescript-eslint/no-var-requires schema.push(require(`./schema/v${schemaID}`).runSchema); } return schema; diff --git a/src/commands/interface-manager/CommandReader.ts b/src/commands/interface-manager/CommandReader.ts index d6bbbf6a..a32ff744 100644 --- a/src/commands/interface-manager/CommandReader.ts +++ b/src/commands/interface-manager/CommandReader.ts @@ -1,63 +1,98 @@ -/** - * Copyright (C) 2022 Gnuxie - * All rights reserved. - */ +// SPDX-FileCopyrightText: 2022 - 2024 Gnuxie +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileAttributionText: +// This modified file incorporates work from super-cool-stream +// https://github.com/Gnuxie/super-cool-stream +// import { MatrixEventReference, MatrixRoomReference, Permalinks, UserID, isError, isStringRoomAlias, isStringRoomID } from "matrix-protection-suite"; +export interface SuperCoolStream { + readonly source: Sequence; + peekItem(eof?: EOF): Item | EOF; + readItem(eof?: EOF): Item | EOF; + getPosition(): number; + setPosition(n: number): void; + clone(): SuperCoolStream; + savingPositionIf(description: { + predicate: (t: Result) => boolean; + body: (stream: SuperCoolStream) => Result; + }): Result; + } -export interface ISuperCoolStream { - readonly source: T - peekItem(eof?: any): T|any, - readItem(eof?: any): T|any, - getPosition(): number, - savingPositionIf(description: { predicate: (t: Result) => boolean, body: (stream: ISuperCoolStream) => Result}): Result; -} + interface Indexable { + at(position: number): Item | undefined; + } -export class SuperCoolStream any|undefined}> implements ISuperCoolStream { - protected position: number + export class StandardSuperCoolStream> + implements SuperCoolStream + { + protected position: number; /** * Makes the super cool string stream. * @param source A string to act as the source of the stream. * @param start Where in the string we should start reading. */ - constructor(public readonly source: T, start = 0) { - this.position = start; + constructor( + public readonly source: Sequence, + start = 0 + ) { + this.position = start; } - public peekItem(eof = undefined) { - return this.source.at(this.position) ?? eof; + public peekItem(eof?: EOF): Item | EOF { + return this.source.at(this.position) ?? (eof as EOF); } - public readItem(eof = undefined) { - return this.source.at(this.position++) ?? eof; + public readItem(eof?: EOF) { + return this.source.at(this.position++) ?? (eof as EOF); } public getPosition(): number { - return this.position; + return this.position; } - savingPositionIf(description: { predicate: (t: Result) => boolean; body: (stream: SuperCoolStream) => Result; }): Result { - const previousPosition = this.position; - const bodyResult = description.body(this); - if (description.predicate(bodyResult)) { - this.position = previousPosition; - } - return bodyResult; + public setPosition(n: number) { + this.position = n; } -} -/** - * Helper for peeking and reading character by character. - */ -class StringStream extends SuperCoolStream { - public peekChar(...args: any[]) { - return this.peekItem(...args); + public clone(): SuperCoolStream { + return new StandardSuperCoolStream(this.source, this.position); } - public readChar(...args: any[]) { - return this.readItem(...args); + savingPositionIf(description: { + predicate: (t: Result) => boolean; + body: (stream: SuperCoolStream) => Result; + }): Result { + const previousPosition = this.position; + const bodyResult = description.body(this); + if (description.predicate(bodyResult)) { + this.position = previousPosition; + } + return bodyResult; } -} + } + + /** + * Helper for peeking and reading character by character. + */ + export class StringStream extends StandardSuperCoolStream< + string, + Indexable + > { + public peekChar(eof?: EOF) { + return this.peekItem(eof); + } + + public readChar(eof?: EOF) { + return this.readItem(eof); + } + + public clone(): StringStream { + return new StringStream(this.source, this.position); + } + } /** Whitespace we want to nom. */ const WHITESPACE = [' ', '\r', '\f', '\v', '\n', '\t']; @@ -95,8 +130,10 @@ export function readCommand(string: string): ReadItem[] { function readCommandFromStream(stream: StringStream): ReadItem[] { const words: ReadItem[] = []; - while (stream.peekChar() !== undefined && (eatWhitespace(stream), true) && stream.peekChar() !== undefined) { + eatWhitespace(stream); + while (stream.peekChar() !== undefined) { words.push(readItem(stream)); + eatWhitespace(stream); } return words.map(applyPostReadTransformersToReadItem); } @@ -117,13 +154,16 @@ function readItem(stream: StringStream): ReadItem { if (WHITESPACE.includes(stream.peekChar())) { throw new TypeError('whitespace should have been stripped'); } - const dispatchCharacter = stream.peekChar()!; + const dispatchCharacter = stream.peekChar(); + if (dispatchCharacter === undefined) { + throw new TypeError(`There should be a dispatch character and if there isn't then the code is wrong`); + } const macro = WORD_DISPATCH_CHARACTERS.get(dispatchCharacter); if (macro) { return macro(stream); } else { // Then read a normal word. - const word: string[] = [stream.readChar()!]; + const word: string[] = [stream.readChar()]; readUntil(/\s/, stream, word); return word.join(''); } @@ -173,7 +213,7 @@ function definePostReadReplace(regex: RegExp, transformer: PostReadStringReplace function applyPostReadTransformersToReadItem(item: ReadItem): ReadItem { if (typeof item === 'string') { - for (const [_key, { regex, transformer }] of POST_READ_TRANSFORMERS) { + for (const [, { regex, transformer }] of POST_READ_TRANSFORMERS) { if (regex.test(item)) { return transformer(item); } @@ -203,7 +243,7 @@ function readUntil(regex: RegExp, stream: StringStream, output: string[]) { * @returns A MatrixRoomReference or string if what has been read does not represent a room. */ function readRoomIDOrAlias(stream: StringStream): MatrixRoomReference|string { - const word: string[] = [stream.readChar()!]; + const word: string[] = [stream.readChar()]; readUntil(/[:\s]/, stream, word); if (stream.peekChar() === undefined || WHITESPACE.includes(stream.peekChar())) { return word.join(''); @@ -226,7 +266,7 @@ defineReadItem('!', readRoomIDOrAlias); * Read the word as a UserID, otherwise return a string if what has been read doesn not represent a user. */ defineReadItem('@', (stream: StringStream): UserID|string => { - const word: string[] = [stream.readChar()!]; + const word: string[] = [stream.readChar()]; readUntil(/[:\s]/, stream, word); if (stream.peekChar() === undefined || WHITESPACE.includes(stream.peekChar())) { return word.join(''); @@ -260,7 +300,7 @@ function readKeyword(stream: StringStream): Keyword { if (stream.peekChar() === undefined) { return new Keyword(''); } - const word: string[] = [stream.readChar()!] + const word: string[] = [stream.readChar()] readUntil(/[\s]/, stream, word) return new Keyword(word.join('')); } diff --git a/src/commands/interface-manager/DeadDocument.ts b/src/commands/interface-manager/DeadDocument.ts index ceb7434f..61d70f7d 100644 --- a/src/commands/interface-manager/DeadDocument.ts +++ b/src/commands/interface-manager/DeadDocument.ts @@ -3,7 +3,7 @@ * All rights reserved. */ -import { SuperCoolStream } from "./CommandReader"; +import { StandardSuperCoolStream } from "./CommandReader"; /** * The DeadDocument started as a universal document object model like Pandoc is. @@ -265,7 +265,7 @@ const COMMITTABLE_NODES = new Set([ NodeTag.Root, ]); -class FringeStream extends SuperCoolStream { +class FringeStream extends StandardSuperCoolStream { } @@ -315,6 +315,9 @@ export class FringeWalker { } while (this.stream.peekItem() && !isAnnotatedNodeCommittable(this.stream.peekItem())) { const annotatedNode = this.stream.readItem(); + if (annotatedNode === undefined) { + throw new TypeError(`Stream code is wrong`); + } switch (annotatedNode.type) { case FringeType.Pre: renderInnerNode(annotatedNode); diff --git a/src/commands/interface-manager/ParameterParsing.ts b/src/commands/interface-manager/ParameterParsing.ts index 5dc8372f..c6cb7c55 100644 --- a/src/commands/interface-manager/ParameterParsing.ts +++ b/src/commands/interface-manager/ParameterParsing.ts @@ -25,11 +25,12 @@ limitations under the License. */ import { ActionError, ActionResult, Ok, ResultError, isError } from "matrix-protection-suite"; -import { ISuperCoolStream, Keyword, ReadItem, SuperCoolStream } from "./CommandReader"; +import { Keyword, ReadItem, StandardSuperCoolStream, SuperCoolStream } from "./CommandReader"; import { PromptOptions } from "./PromptForAccept"; import { PromptRequiredError } from "./PromptRequiredError"; +import { printReadably } from "./PrintReadably"; -export interface IArgumentStream extends ISuperCoolStream { +export interface IArgumentStream extends SuperCoolStream { rest(): ReadItem[], // All of the read items before the current position. priorItems(): ReadItem[], @@ -38,7 +39,7 @@ export interface IArgumentStream extends ISuperCoolStream { prompt(parameterDescription: ParameterDescription): Promise>, } -export class ArgumentStream extends SuperCoolStream implements IArgumentStream { +export class ArgumentStream extends StandardSuperCoolStream implements IArgumentStream { public rest() { return this.source.slice(this.position); } @@ -96,7 +97,7 @@ export function simpleTypeValidator(name: string, predicate: (readItem: ReadItem return Ok(result); } else { // How do we accurately denote the type when it includes spaces in its name, same for the read item? - return ActionError.Result(`Was expecting a match for the presentation type: ${name} but got ${readItem}.`); + return ActionError.Result(`Was expecting a match for the presentation type: ${name} but got ${printReadably(readItem)}.`); } } } @@ -115,6 +116,7 @@ export function presentationTypeOf(presentation: unknown): PresentationType|unde } else { // until there are subtype semantics we have to fail early so that we have a chance of knowing // that we have a conflicting type. + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new TypeError(`presentationTypeof: There are multiple candidates for the presentation ${presentation}: ${JSON.stringify(candidates.map(c => c.name))}`) } } @@ -166,7 +168,7 @@ export class RestDescription implements ParameterDesc */ public async parseRest(stream: IArgumentStream, promptForRest: boolean, keywordParser: KeywordParser): Promise> { const items: ReadItem[] = []; - if (this.prompt && promptForRest && stream.isPromptable() && stream.peekItem() === undefined) { + if (this.prompt && promptForRest && stream.isPromptable() && stream.peekItem(undefined) === undefined) { return PromptRequiredError.Result( `A prompt is required for the missing argument for the ${this.name} parameter`, { @@ -175,20 +177,22 @@ export class RestDescription implements ParameterDesc } ); } - while (stream.peekItem() !== undefined) { + while (stream.peekItem(undefined) !== undefined) { const keywordResult = keywordParser.parseKeywords(stream); if (isError(keywordResult)) { return keywordResult; } - if (stream.peekItem() !== undefined) { - const validationResult = this.acceptor.validator(stream.peekItem()); + const keywordValue = stream.peekItem(undefined); + if (keywordValue !== undefined) { + const validationResult = this.acceptor.validator(keywordValue); if (isError(validationResult)) { return ArgumentParseError.Result( validationResult.error.message, { parameter: this, stream } ); } - items.push(stream.readItem()); + items.push(keywordValue); + stream.readItem(); // dispose of keyword's associated value from the stream. } } return Ok(items); diff --git a/src/config.ts b/src/config.ts index 9d59f009..27db20fc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -55,7 +55,7 @@ export interface IConfig { */ autojoinOnlyIfManager: boolean; /** Mjolnir will accept invites from members of this space if `autojoinOnlyIfManager` is false. */ - acceptInvitesFromSpace: string; + acceptInvitesFromSpace: string | undefined; recordIgnoredInvites: boolean; managementRoom: string; verboseLogging: boolean; @@ -281,7 +281,7 @@ export function getProvisionedMjolnirConfig(managementRoomId: string): IConfig { } const config = Config.util.extendDeep( getDefaultConfig(), - allowedKeys.reduce((existingConfig: any, key: string) => { + allowedKeys.reduce((existingConfig, key) => { return { ...existingConfig, [key]: configTemplate[key as keyof IConfig] } }, {}) );