Skip to content

Commit a866dfc

Browse files
committed
feat(commands): new way to register commands
Use SpectrumState.commands.registerCommands() method to quickly register commands either via a glob or a static array BREAKING CHANGE: You cannot instantiate SpectrumCommands on your own, but need to use the SpectrumState.commands instance instead.
1 parent 06b30b7 commit a866dfc

File tree

9 files changed

+264
-25
lines changed

9 files changed

+264
-25
lines changed

spec/commands.spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Container } from "typedi";
2+
import { SpectrumLobby } from "./../src/";
3+
import { SpectrumChannel } from "./../src/";
4+
import { SpectrumBroadcaster } from "./../src/";
5+
import { SpectrumCommunity, SpectrumCommands } from "../src/";
6+
import { TestInstance } from "./_.instance";
7+
import { TestShared } from "./_.shared";
8+
9+
import {} from "jasmine";
10+
import { SpectrumCommand } from "../src/Spectrum/components/api/decorators/spectrum-command.decorator";
11+
12+
describe("Spectrum Commands", () => {
13+
TestShared.commonSetUp();
14+
15+
let commands: SpectrumCommands;
16+
beforeEach(() => {
17+
commands = TestInstance.bot.getState().commands;
18+
});
19+
20+
describe("Service", () => {
21+
it("Should expose the command service", async () => {
22+
expect(commands).toBeTruthy();
23+
expect(commands instanceof SpectrumCommands).toBe(true);
24+
});
25+
26+
it("Should register commands through glob", async () => {
27+
await commands.registerCommands({ commands: ["spec/mock/commands/*.ts"] });
28+
29+
const registered = commands.getCommandList();
30+
31+
expect(registered.find(command => command.shortCode === "testCommand1")).toBeTruthy();
32+
expect(registered.find(command => command.shortCode === "testCommand2")).toBeTruthy();
33+
});
34+
35+
it("Should register commands through array", async () => {
36+
const test3 = class {
37+
callback = () => {};
38+
};
39+
SpectrumCommand("test3")(test3);
40+
41+
const test4 = class {
42+
callback = () => {};
43+
};
44+
SpectrumCommand("test4")(test4);
45+
await commands.registerCommands({ commands: [test3, test4] });
46+
47+
const registered = commands.getCommandList();
48+
49+
expect(registered.find(command => command.shortCode === "test3")).toBeTruthy();
50+
expect(registered.find(command => command.shortCode === "test4")).toBeTruthy();
51+
});
52+
53+
it("Should not register commands that are not decorated", async () => {
54+
const test = class {
55+
callback = () => {};
56+
};
57+
58+
await commands.registerCommands({ commands: [test] });
59+
60+
const registered = commands.getCommandList();
61+
62+
expect(registered.find(command => command.shortCode === "test5")).toBeUndefined();
63+
});
64+
65+
it("Should call the callback provided on", done => {
66+
TestShared.testLobbyDependentSetUp();
67+
TestInstance.lobby = TestInstance.bot
68+
.getState()
69+
.getCommunityByName(TestShared.config._testCommunity)
70+
.getLobbyByName(TestShared.config._testLobby);
71+
commands.setPrefix("[UNIT]");
72+
73+
const testCallback = class {
74+
callback = () => {
75+
TestInstance.lobby.unSubscribe();
76+
done();
77+
};
78+
};
79+
SpectrumCommand("test callback")(testCallback);
80+
81+
commands.getCommandList();
82+
83+
TestInstance.lobby.subscribe();
84+
TestInstance.lobby.sendPlainTextMessage("[UNIT] test callback");
85+
});
86+
});
87+
88+
describe("Decorator", () => {
89+
it("Should decorate the class", () => {
90+
const test = class {
91+
callback = () => {};
92+
};
93+
SpectrumCommand("test")(test);
94+
expect(Reflect.getMetadata("spectrum-command", test)).toBe(true);
95+
});
96+
});
97+
});

spec/lobby.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ describe("Text Lobby", () => {
2222
});
2323

2424
it("Should be able to subscribe to a lobby", () => {
25-
expect(TestInstance.lobby.isSubscribed()).toBe(false);
2625
TestInstance.lobby.subscribe();
2726
expect(TestInstance.lobby.isSubscribed()).toBe(true);
2827
});

spec/mock/commands/command1.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { SpectrumCommand } from "../../../src/Spectrum/components/api/decorators/spectrum-command.decorator";
2+
3+
@SpectrumCommand("testCommand1")
4+
export class TestCommand1 {
5+
public callback() {
6+
7+
}
8+
}

spec/mock/commands/command2.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { SpectrumCommand } from "../../../src/Spectrum/components/api/decorators/spectrum-command.decorator";
2+
3+
@SpectrumCommand("testCommand2")
4+
export class TestCommand2 {
5+
public callback() {
6+
7+
}
8+
}

src/Spectrum/components/api/command.component.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
* @module Spectrum
33
*/ /** */
44

5-
import { ISpectrumCommunity } from "../../interfaces/spectrum/community/community.interface";
6-
import { ISpectrumLobby } from "../../interfaces/spectrum/community/chat/lobby.interface";
75
import { aSpectrumCommand } from "../../interfaces/api/command.interface";
8-
import { SpectrumLobby } from "../chat/lobby.component";
96

107
/**
11-
* #
128
* Helper class for a Bot Command
139
*
10+
* This class is internal and should not be used anymore to create your own commands.
11+
* Please see SpectrumCommand decorator instead
12+
*
1413
* @class aBotCommand
1514
*/
1615
export class aBotCommand implements aSpectrumCommand {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @module Spectrum
3+
*/ /** */
4+
import { Container } from "typedi";
5+
import { aSpectrumCommand } from "../../../interfaces/api/command.interface";
6+
import { SpectrumCommands } from "../../../services/commands.service";
7+
8+
/**
9+
* Decorate a class as being a command a bot can listen to
10+
*
11+
* **example:**
12+
*```typescript
13+
* @SpectrumCommand("my-command")
14+
* export class MyCommand {
15+
* public async callback(textMessage, lobby) {
16+
* // This will be called automatically when someone says "my-command"
17+
* }
18+
*}
19+
* ```
20+
*
21+
* Do **not** forget to register your command via SpectrumCommands.registerCommands()
22+
*/
23+
export function SpectrumCommand(opts: SpectrumCommandOpts): SpectrumCommandHandlerClass;
24+
export function SpectrumCommand(
25+
shortCode: SpectrumCommandOpts["shortCode"]
26+
): SpectrumCommandHandlerClass;
27+
export function SpectrumCommand(
28+
opts: SpectrumCommandOpts | SpectrumCommandOpts["shortCode"]
29+
): SpectrumCommandHandlerClass {
30+
let options: SpectrumCommandOpts;
31+
if (typeof opts === "string" || opts instanceof RegExp) {
32+
options = { shortCode: opts };
33+
} else {
34+
options = { ...opts };
35+
}
36+
37+
return (
38+
Class: new (...any: any[]) => {
39+
callback: aSpectrumCommand["callback"];
40+
}
41+
) => {
42+
Reflect.defineMetadata("spectrum-command", true, Class);
43+
const instance = new Class();
44+
Container.get(SpectrumCommands).registerCommand(
45+
options.name || "",
46+
options.shortCode,
47+
instance.callback.bind(instance),
48+
options.manual || ""
49+
);
50+
};
51+
}
52+
53+
export type SpectrumCommandHandlerClass = (target: SpectrumCommandMakable) => void;
54+
55+
export type SpectrumCommandMakable = new (...any: any[]) => {
56+
callback: aSpectrumCommand["callback"];
57+
};
58+
59+
/**
60+
* Options for constructing a spectrum bot command
61+
*/
62+
export interface SpectrumCommandOpts {
63+
/**
64+
* the code that will trigger this command, excluding the prefix.
65+
* In case of a regex with capture groups, the resulting matches will be provided to the callback function
66+
*
67+
* **Example:**
68+
* *(prefix set to !bot)*
69+
*
70+
* shortCode: `testCommand`
71+
* Will match `!bot testCommand`
72+
**/
73+
shortCode: string | RegExp;
74+
/** pretty name to be given internally to this command */
75+
name?: string;
76+
/** manual entry for this command */
77+
manual?: string;
78+
}

src/Spectrum/components/chat/lobby.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ export class SpectrumLobby {
253253
};
254254
}
255255

256+
/**
257+
* not implemented yet
258+
*/
256259
public unSubscribe() {}
257260

258261
/**

src/Spectrum/services/commands.service.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { SpectrumLobby } from "../components/chat/lobby.component";
1010
import { receivedTextMessage } from "../interfaces/spectrum/community/chat/receivedTextMessage.interface";
1111
import { Service } from "typedi";
1212
import { TSMap } from "typescript-map";
13-
13+
import { SpectrumCommandMakable } from "../components/api/decorators/spectrum-command.decorator";
14+
import * as requireGlob from "require-glob";
15+
import "reflect-metadata";
1416
/** @class SpectrumCommand */
1517
@Service()
1618
export class SpectrumCommands {
@@ -21,7 +23,7 @@ export class SpectrumCommands {
2123
/** The prefix for the commands */
2224
protected prefix: string = "\\spbot";
2325
/** Map of commands */
24-
protected _commandMap: TSMap<string, aSpectrumCommand> = new TSMap<string, aSpectrumCommand>();
26+
protected _commandMap: Map<string, aSpectrumCommand> = new Map<string, aSpectrumCommand>();
2527

2628
constructor() {
2729
this.Broadcaster.addListener("message.new", this.checkForCommand);
@@ -57,16 +59,20 @@ export class SpectrumCommands {
5759
// }
5860
// }
5961

60-
this._commandMap.forEach((value: aSpectrumCommand, key: string) => {
61-
let re = new RegExp("^" + key);
62+
for (let [key, value] of this._commandMap.entries()) {
63+
let re = new RegExp(`^${this.escapeRegExp(this.prefix)} ${key}`);
6264
let matches = messageAsLower.match(re);
6365
if (matches) {
6466
value.callback(payload.message, lobby, matches);
6567
return; // there cant be 2 commands can there?
6668
}
67-
});
69+
}
6870
};
6971

72+
/**
73+
* Set the prefix for every commands. Any text message not starting with this prefix will be ignored
74+
* (case insensitive)
75+
*/
7076
public setPrefix(prefix: string) {
7177
this.prefix = prefix.toLowerCase();
7278
}
@@ -79,15 +85,16 @@ export class SpectrumCommands {
7985
* @param shortCode the shortcode to listen for
8086
* @param callback the function to call when this command is used
8187
* @param manual an explanation of what this command does.
88+
* @deprecated use registerCommands instead
8289
* @return the aSpectrumCommand object that we are now listening for.
8390
*/
8491
public registerCommand(command: aSpectrumCommand): aSpectrumCommand;
8592
public registerCommand(name: string, shortCode, callback, manual): aSpectrumCommand;
8693
public registerCommand(
8794
name: string | aSpectrumCommand,
88-
shortCode?,
89-
callback?,
90-
manual?
95+
shortCode?: string,
96+
callback?: Function,
97+
manual?: string
9198
): aSpectrumCommand {
9299
var co = null;
93100
if (typeof name === typeof "test") {
@@ -96,33 +103,67 @@ export class SpectrumCommands {
96103
co = name;
97104
}
98105

99-
var commandString = this.prefix.toLowerCase() + " " + co.shortCode.toLowerCase();
106+
var commandString = co.shortCode.toLowerCase();
100107
this._commandMap.set(commandString, co);
101108

102109
return co;
103110
}
104111

105112
/**
106113
* Alias of registerCommand
114+
* @deprecated use registerCommands instead
107115
*/
108116
public addCommand(name, shortCode, callback, manual) {
109117
return this.registerCommand(name, shortCode, callback, manual);
110118
}
111119

112120
/**
113121
* Unbinds a command and stop listening to it.
122+
* @todo
114123
*/
115124
public unRegisterCommand(command: aBotCommand);
116125
public unRegisterCommand(commandId: number);
117126
public unRegisterCommand(co) {
118127
let shortcodeAsLower = co.shortCode.toLowerCase();
119-
120-
this._commandMap.filter(function(command, key) {
121-
return key === shortcodeAsLower;
122-
});
123128
}
124129

130+
/**
131+
* Return the list of commands currently registered and active
132+
*/
125133
public getCommandList(): aSpectrumCommand[] {
126-
return this._commandMap.values();
134+
return Array.from(this._commandMap.values());
135+
}
136+
137+
/**
138+
* Register a batch of commands, either as an array or with a glob.
139+
* Commands __must be decorated__ with @SpectrumCommand() decorator
140+
*/
141+
public async registerCommands(opts: {
142+
/** array of actual commands or globs to import them */
143+
commands: SpectrumCommandMakable[] | string[];
144+
}) {
145+
if (opts.commands.length === 0) return;
146+
147+
if (typeof opts.commands[0] === "string") {
148+
// Import as glob, we have nothing to do.
149+
await requireGlob((opts.commands as string[]).map(path => `${process.cwd()}/${path}`));
150+
} else {
151+
await Promise.all(
152+
(opts.commands as SpectrumCommandMakable[]).map(async Command => {
153+
// We just make sure it's decorated and that we have nothing to do.
154+
if (!Reflect.getMetadata("spectrum-command", Command)) {
155+
console.error(
156+
`[ERROR] Could not register command ${
157+
Command.constructor.name
158+
}. Did you forget to decorate it with @SpectrumCommand() ?`
159+
);
160+
}
161+
})
162+
);
163+
}
164+
}
165+
166+
protected escapeRegExp(string: string) {
167+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
127168
}
128169
}

0 commit comments

Comments
 (0)