-
Notifications
You must be signed in to change notification settings - Fork 56
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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. |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep
src/commands/CommandHandler.ts
Outdated
@@ -181,3 +183,6 @@ export async function handleCommand(roomId: string, event: { content: { body: st | |||
return await mjolnir.client.sendMessage(roomId, reply); | |||
} | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
src/models/PolicyList.ts
Outdated
@@ -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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that change related?
@@ -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); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
mjolnir: Mjolnir, | ||
roomId: string, | ||
event: any, | ||
parts: string[]) => Promise<Parameters<ExecutorType>>; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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[]; |
There was a problem hiding this comment.
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?
3e2e505
to
3fa1a43
Compare
There was a problem hiding this 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?
/** | ||
* 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 | ||
*/ |
There was a problem hiding this comment.
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
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") | ||
); |
There was a problem hiding this comment.
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
* Used by ApplicationCommands as required feature flags they depend on to function. | ||
*/ | ||
export interface ApplicationFeature { | ||
name: string, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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:
- Something in my code is broken. In JS, that's generally represented by a
TypeError
(even if it's not a type error, sigh) orSyntaxError
(exceptSyntaxError
is also used when parsing JSON, sigh). - Something in my environment is broken. In JS, that's generally other cases of
Error
. - 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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.