This commit is contained in:
David Teller 2022-07-19 18:25:29 +02:00
parent 3cb4ffc3e6
commit da4edb8854
4 changed files with 59 additions and 58 deletions

View File

@ -48,14 +48,14 @@ export const COMMAND_PREFIX = "!mjolnir";
export async function handleCommand(roomId: string, event: { content: { body: string } }, mjolnir: Mjolnir) {
const line = event['content']['body'];
const parts = line.trim().split(' ').filter(p => p.trim().length > 0);
console.debug("YORIC", "line", line);
const lexer = new Lexer(line);
lexer.token("command"); // Consume `!mjolnir`.
// Extract command.
const cmd = lexer.alternatives(
() => lexer.token("id").text,
() => null
);
console.debug("YORIC", "cmd", cmd);
try {
if (cmd === null || cmd === 'status') {

View File

@ -6,92 +6,98 @@ import * as TokenizrModule from "tokenizr";
import { parseDuration } from "../utils";
const TokenizrClass = Tokenizr || TokenizrModule;
const WHITESPACE = /\s+/;
const COMMAND = /![a-zA-Z_]+/;
const IDENTIFIER = /[a-zA-Z_]+/;
const USER_ID = /@[a-zA-Z0-9_.=\-/]+:\S+/;
const GLOB_USER_ID = /@[a-zA-Z0-9_.=\-?*/]+:\S+/;
const ROOM_ID = /![a-zA-Z0-9_.=\-/]+:\S+/;
const ROOM_ALIAS = /#[a-zA-Z0-9_.=\-/]+:\S+/;
const ROOM_ALIAS_OR_ID = /[#!][a-zA-Z0-9_.=\-/]+:\S+/;
const INT = /[+-]?[0-9]+/;
const STRING = /"((?:\\"|[^\r\n])*)"/;
const DATE_OR_DURATION = /(?:"([^"]+)")|([^"]\S+)/;
const STAR = /\*/;
const ETC = /.*$/;
/**
* A lexer for common cases.
* A lexer for command parsing.
*
* Recommended use is `lexer.token("state")`.
*/
export class Lexer extends TokenizrClass {
constructor(string: string) {
super();
console.debug("YORIC", "Lexer", 0);
// Ignore whitespace.
this.rule(/\s+/, (ctx) => {
this.rule(WHITESPACE, (ctx) => {
ctx.ignore()
})
// Command rules, e.g. `!mjolnir`
this.rule("command", /![a-zA-Z_]+/, (ctx) => {
this.rule("command", COMMAND, (ctx) => {
ctx.accept("command");
});
// Identifier rules, used e.g. for subcommands `get`, `set` ...
this.rule("id", /[a-zA-Z_]+/, (ctx) => {
this.rule("id", IDENTIFIER, (ctx) => {
ctx.accept("id");
});
// Users
this.rule("userID", /@[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
this.rule("userID", USER_ID, (ctx) => {
ctx.accept("userID");
});
this.rule("globUserID", /@[a-zA-Z0-9_.=\-?*/]+:.+/, (ctx) => {
this.rule("globUserID", GLOB_USER_ID, (ctx) => {
ctx.accept("globUserID");
});
// Rooms
this.rule("roomID", /![a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
this.rule("roomID", ROOM_ID, (ctx) => {
ctx.accept("roomID");
});
this.rule("roomAlias", /#[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
this.rule("roomAlias", ROOM_ALIAS, (ctx) => {
ctx.accept("roomAlias");
});
this.rule("roomAliasOrID", /[#!][a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
this.rule("roomAliasOrID", ROOM_ALIAS_OR_ID, (ctx) => {
ctx.accept("roomAliasOrID");
});
// Numbers.
this.rule("int", /[+-]?[0-9]+/, (ctx, match) => {
this.rule("int", INT, (ctx, match) => {
ctx.accept("int", parseInt(match[0]))
});
// Quoted strings.
this.rule("string", /"((?:\\"|[^\r\n])*)"/, (ctx, match) => {
this.rule("string", STRING, (ctx, match) => {
ctx.accept("string", match[1].replace(/\\"/g, "\""))
});
// Dates and durations.
console.debug("YORIC", "Lexer", 1);
try {
this.rule("dateOrDuration", /(?:"([^"]+)")|(\S+)/, (ctx, match) => {
let content = match[1] || match[2];
console.debug("YORIC", "Lexer", "dateOrDuration", content);
let date = new Date(content);
console.debug("YORIC", "Lexer", "dateOrDuration", "date", date);
if (!date || Number.isNaN(date.getDate())) {
let duration = parseDuration(content);
console.debug("YORIC", "Lexer", "dateOrDuration", "duration", duration);
if (!duration || Number.isNaN(duration)) {
ctx.reject();
} else {
ctx.accept("duration", duration);
}
this.rule("dateOrDuration", DATE_OR_DURATION, (ctx, match) => {
let content = match[1] || match[2];
let date = new Date(content);
if (!date || Number.isNaN(date.getDate())) {
let duration = parseDuration(content);
if (!duration || Number.isNaN(duration)) {
ctx.reject();
} else {
ctx.accept("date", date);
ctx.accept("duration", duration);
}
});
} catch (ex) {
console.error("YORIC", ex);
}
} else {
ctx.accept("date", date);
}
});
// Jokers.
this.rule("STAR", /\*/, (ctx) => {
this.rule("STAR", STAR, (ctx) => {
ctx.accept("STAR");
});
// Everything left in the string.
this.rule("ETC", /.*/, (ctx) => {
ctx.accept("ETC")
this.rule("ETC", ETC, (ctx, match) => {
ctx.accept("ETC", match[0].trim());
});
console.debug("YORIC", "Preparing lexer", string);
this.input(string);
}

View File

@ -77,7 +77,6 @@ function formatResult(action: string, targetRoomId: string, recentJoins: Join[],
async function execSinceCommandAux(destinationRoomId: string, event: any, mjolnir: Mjolnir, lexer: Lexer): Promise<Result<undefined>> {
// Attempt to parse `<date/duration>` as a date or duration.
let dateOrDuration: Date |number = lexer.token("dateOrDuration").value;
console.debug("YORIC", "dateOrDuration", dateOrDuration);
let minDate;
let maxAgeMS;
if (dateOrDuration instanceof Date) {
@ -101,25 +100,28 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
if (!action) {
return {error: `Invalid <action>. Expected one of ${JSON.stringify(Action)}`};
}
console.debug("YORIC", "action", action);
// Attempt to parse `<limit>` as a number.
const maxEntries = lexer.token("int").value as number;
console.debug("YORIC", "maxEntries", maxEntries);
// Now list affected rooms.
// Parse rooms.
// Parse everything else as `<reason>`, stripping quotes if any have been added.
const rooms: Set</* room id */string> = new Set();
let reason = "";
do {
let token = lexer.alternatives(
// Room
() => lexer.token("STAR"),
() => lexer.token("roomAliasOrID"),
// Reason
() => lexer.token("string"),
() => lexer.token("ETC")
);
console.debug("YORIC", "token", token);
if (!token) {
// We have reached the end of rooms.
if (!token || token.type === "EOF") {
// We have reached the end of rooms, no reason.
break;
}
if (token.type === "STAR") {
} else if (token.type === "STAR") {
for (let roomId of Object.keys(mjolnir.protectedRooms)) {
rooms.add(roomId);
}
@ -131,8 +133,9 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
}
rooms.add(roomId);
continue;
}
if (token.type == 'EOF') {
} else if (token.type === "string" || token.type === "ETC") {
// We have reached the end of rooms with a reason.
reason = token.text;
break;
}
} while(true);
@ -141,14 +144,6 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
error: "Missing rooms. Use `*` if you wish to apply to every protected room.",
};
}
console.debug("YORIC", "rooms", rooms);
// Parse everything else as `<reason>`, stripping quotes if any have been added.
const reason = lexer.alternatives(
() => lexer.token("string"),
() => lexer.token("ETC")
)?.text || "";
console.debug("YORIC", "reason", reason);
const progressEventId = await mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '⏳');

View File

@ -12,7 +12,7 @@ export const mochaHooks = {
console.error("---- entering test", JSON.stringify(this.currentTest.title)); // Makes MatrixClient error logs a bit easier to parse.
console.log("mochaHooks.beforeEach");
// Sometimes it takes a little longer to register users.
this.timeout(10000)
this.timeout(20000)
this.managementRoomAlias = config.managementRoom;
this.mjolnir = await makeMjolnir();
config.RUNTIME.client = this.mjolnir.client;