Skip to content

Commit 960df25

Browse files
committed
Initial stab at support bot commands
1 parent 67fb51d commit 960df25

File tree

2 files changed

+92
-1
lines changed

2 files changed

+92
-1
lines changed

src/components/bot-commands.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import markdown from "markdown-it";
2+
import stringArgv from "string-argv";
3+
import { MatrixMessageContent } from "./MatrixEvent";
4+
5+
const md = new markdown();
6+
7+
export const botCommandSymbol = Symbol("botCommandMetadata");
8+
export function botCommand(prefix: string, help: string, requiredArgs: string[] = [], optionalArgs: string[] = [], includeUserId = false) {
9+
return Reflect.metadata(botCommandSymbol, {
10+
prefix,
11+
help,
12+
requiredArgs,
13+
optionalArgs,
14+
includeUserId,
15+
});
16+
}
17+
18+
type BotCommandFunction = (...args: string[]) => Promise<{status: boolean}>;
19+
20+
export type BotCommands = {[prefix: string]: {
21+
fn: BotCommandFunction,
22+
requiredArgs: string[],
23+
optionalArgs: string[],
24+
includeUserId: boolean,
25+
}};
26+
27+
/**
28+
* Compile a prototype with a set of bot command functions (functions that are decorated with `botCommand`)
29+
* @param prototype A class prototype containing a set of `botCommand` decorated functions.
30+
* @returns
31+
*/
32+
export function compileBotCommands(prototype: Record<string, BotCommandFunction>): {helpMessage: (cmdPrefix?: string) => MatrixMessageContent, botCommands: BotCommands} {
33+
let content = "Commands:\n";
34+
const botCommands: BotCommands = {};
35+
Object.getOwnPropertyNames(prototype).forEach(propetyKey => {
36+
const b = Reflect.getMetadata(botCommandSymbol, prototype, propetyKey);
37+
if (b) {
38+
const requiredArgs = b.requiredArgs.join(" ");
39+
const optionalArgs = b.optionalArgs.map((arg: string) => `[${arg}]`).join(" ");
40+
content += ` - \`££PREFIX££${b.prefix}\` ${requiredArgs} ${optionalArgs} - ${b.help}\n`;
41+
// We know that this is safe.
42+
botCommands[b.prefix as string] = {
43+
fn: prototype[propetyKey],
44+
requiredArgs: b.requiredArgs,
45+
optionalArgs: b.optionalArgs,
46+
includeUserId: b.includeUserId,
47+
};
48+
}
49+
});
50+
return {
51+
helpMessage: (cmdPrefix?: string) => ({
52+
msgtype: "m.notice",
53+
body: content,
54+
formatted_body: md.render(content).replace(/££PREFIX££/g, cmdPrefix || ""),
55+
format: "org.matrix.custom.html"
56+
}),
57+
botCommands,
58+
}
59+
}
60+
61+
export async function handleCommand(
62+
userId: string, command: string, botCommands: BotCommands, obj: unknown, prefix?: string
63+
): Promise<{error?: string, handled?: boolean, humanError?: string}> {
64+
if (prefix) {
65+
if (!command.startsWith(prefix)) {
66+
return {handled: false};
67+
}
68+
command = command.substring(prefix.length);
69+
}
70+
const parts = stringArgv(command);
71+
for (let i = parts.length; i > 0; i--) {
72+
const cmdPrefix = parts.slice(0, i).join(" ").toLowerCase();
73+
// We have a match!
74+
const command = botCommands[prefix];
75+
if (command) {
76+
if (command.requiredArgs.length > parts.length - i) {
77+
return {error: "Missing args"};
78+
}
79+
const args = parts.slice(i);
80+
if (command.includeUserId) {
81+
args.splice(0,0, userId);
82+
}
83+
try {
84+
await botCommands[prefix].fn.apply(obj, args);
85+
return {handled: true};
86+
} catch (ex) {
87+
return {handled: true, error: ex.message, humanError: ex.humanError};
88+
}
89+
}
90+
}
91+
return {handled: false};
92+
}

src/components/intent.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,6 @@ export class Intent {
834834
return await this.botSdkIntent.underlyingClient.getRoomStateEvent(roomId, eventType, stateKey);
835835
}
836836
catch (ex) {
837-
console.log("getStateEvent", ex);
838837
if (ex.body.errcode !== "M_NOT_FOUND" || !returnNull) {
839838
throw ex;
840839
}

0 commit comments

Comments
 (0)