Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First attempt at refactoring commands. #403

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

Gnuxie
Copy link
Contributor

@Gnuxie Gnuxie commented Nov 1, 2022

The idea is that commands can be used in other contexts than just from matrix, e.g. the mjolnir appservice web api that is given to the widget.

We also begin organizing commands into tables that can be automatically created from a set of features that are enabled in mjolnir's config (e.g. "synapse admin").

The only commands to have been changes so far is the ban/unban commands. Though the parser logic in that command is horrible.

This doesn't attempt to change the way we parse commands from Matrix, but I do plan to change that too, unsure of whether that will be in this PR though.

The PR is a draft because I want initial comments on the approach/design before i commit and rewrite more of the existing command handler.

@Gnuxie Gnuxie requested review from Yoric and jesopo November 1, 2022 15:00
Copy link
Contributor

@Yoric Yoric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sounds like a sound plan.

result: Awaited<ExecutorReturnType>) => Promise<void>;

/**
* Note, matrix interface command can be multi step ie ask for confirmation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Generally speaking, documentation for a class should start with a description of what the class does, before any note. Here and for other classes.

@@ -126,6 +123,11 @@ export async function handleCommand(roomId: string, event: { content: { body: st
} else if (parts[1] === 'make' && parts[2] === 'admin' && parts.length > 3) {
return await execMakeRoomAdminCommand(roomId, event, mjolnir, parts);
} else {
const command = commandTable.findAMatchingCommand(parts.slice(1));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the idea is to apply it gradually, by progressively moving commands from handleCommand? Sounds like a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

@@ -181,3 +183,6 @@ export async function handleCommand(roomId: string, event: { content: { body: st
return await mjolnir.client.sendMessage(roomId, reply);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk";
import { extractRequestError, LogService, MatrixClient, RoomCreateOptions, UserID } from "matrix-bot-sdk";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that change related?

src/commands/ApplicationCommand.ts Show resolved Hide resolved
@@ -204,7 +208,8 @@ export class Mjolnir {
LogService.info("Mjolnir", `Command being run by ${event['sender']}: ${event['content']['body']}`);

await client.sendReadReceipt(roomId, event['event_id']);
return handleCommand(roomId, event, this);

return handleCommand(roomId, event, this, this.matrixCommandTable);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Shouldn't handleCommand be a method of matrixCommandTable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, but can't be until we have moved all the commands to the new table

src/commands/ApplicationCommand.ts Show resolved Hide resolved
src/commands/ApplicationCommand.ts Show resolved Hide resolved
mjolnir: Mjolnir,
roomId: string,
event: any,
parts: string[]) => Promise<Parameters<ExecutorType>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely certain that parts is the right type for these. It makes it impossible to parse things that contains whitespaces. Or perhaps we'll want to add a special case for "any series of words between quotes" and turn them into a single part?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this was mostly for compatibility. I'm not sure how to design the parsing properly yet but I do want this to be replaced.

What i do know though is that I am thinking of something where everything is read from a stream but we can just declare the accepting types for each argument + provide a parse function for specific arguments if they don't have common types (common meaning like a matrix room or mxid).

* This is used by mjolnirs.
*/
export class MatrixCommandTable {
public readonly features: ApplicationFeature[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the list of active features?

@Gnuxie Gnuxie marked this pull request as ready for review November 11, 2022 11:33
Copy link
Contributor Author

@Gnuxie Gnuxie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if you neglected to tell me if I was crazy before, how about now?

Comment on lines +19 to +45
/**
* Why do we need a Result Monad for the parser signiture.
* I (Gnuxie) don't like monadic error handling, simply because
* I'm a strong believer in failing early, yes i may be misinformed.
* The only reason we don't use an exception in this case is because
* these are NOT to be used nilly willy and thrown out of context
* from an unrelated place. The Monad ensures locality (in terms of call chain)
* to the user interface by being infuriating to deal with.
* It also does look different to an exception
* to a naive programmer. Ideally though, if the world had adopted
* condition based error handling i would simply create a condition
* type for validation errors that can be translated/overidden by
* the command handler, and it wouldn't have to look like this.
* It's important to remember the errors we are reporting are to do with user input,
* we're trying to tell the user they did something wrong and what that is.
* This is something completely different to a normal exception,
* where we are saying to ourselves that our assumptions in our code about
* the thing we're doing are completely wrong. The user never
* should see these as there is nothing they can do about it.
*
* OK, it would be too annoying even for me to have a real Monad.
* So this is dumb as hell, no worries though
*
* OK I'm beginning to regret my decision.
*
* TODO: Can we make ValidationResult include ValidationError
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tell me what you think

Comment on lines -105 to +123
return null;
if (list.isErr()) {
return ValidationResult.Err(list.err);
} else if (!ruleType) {
return ValidationResult.Err(
ValidationError.makeValidationError('uknown rule type', "Please specify the type as either 'user', 'room', or 'server'")
);
} else if (!entity) {
return ValidationResult.Err(
ValidationError.makeValidationError('no entity', "No entity was able to be parsed from this command")
);
} else if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) {
return ValidationResult.Err(
ValidationError.makeValidationError("wildcard required", "Wildcard bans require an additional `--force` argument to confirm")
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here as an example

@Gnuxie Gnuxie requested a review from Yoric November 23, 2022 18:01
* Used by ApplicationCommands as required feature flags they depend on to function.
*/
export interface ApplicationFeature {
name: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc: Is this a human-readable name? A unique key? If the latter, possibly rename to key.

*/
const APPLICATION_FEATURES = new Map<string/*feature name*/, ApplicationFeature>();

export function defineApplicationFeature(feature: ApplicationFeature): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Doc needed

* type for validation errors that can be translated/overidden by
* the command handler, and it wouldn't have to look like this.
* It's important to remember the errors we are reporting are to do with user input,
* we're trying to tell the user they did something wrong and what that is.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, generally speaking, there are several kinds of errors and error-handling:

  1. Something in my code is broken. In JS, that's generally represented by a TypeError (even if it's not a type error, sigh) or SyntaxError (except SyntaxError is also used when parsing JSON, sigh).
  2. Something in my environment is broken. In JS, that's generally other cases of Error.
  3. Something in my input is broken. There is nothing built-in in JS, but that could be handled by creating a new variant of Error.

While I'm ok with using monads in the right setting and there are ways to use Monads in JS with some syntactic support, by encoding them as a generator and its caller. However, introducing a different paradigm in the middle of an application feels a bit cumbersome, so I'd like to avoid it if possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, I don't see anything that looks like monadic binding, so this isn't really a monad, more a data structure to represent good parse result/bad parse result, right? That feels ok to me.

What does it bring wrt a new variant of Error, though? Better type-checking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this isn't really a monad, more a data structure to represent good parse result/bad parse result, right? That feels ok to me.

Yep

What does it bring wrt a new variant of Error, though? Better type-checking?

Because as I said, so it doesn't get misused. The validation errors are a description of something that the user has done wrong, for the user to see.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants