Skip to content

Commit

Permalink
feat(commands): new way to register commands
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Superd22 committed Dec 28, 2018
1 parent 06b30b7 commit a866dfc
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 25 deletions.
97 changes: 97 additions & 0 deletions spec/commands.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Container } from "typedi";
import { SpectrumLobby } from "./../src/";
import { SpectrumChannel } from "./../src/";
import { SpectrumBroadcaster } from "./../src/";
import { SpectrumCommunity, SpectrumCommands } from "../src/";
import { TestInstance } from "./_.instance";
import { TestShared } from "./_.shared";

import {} from "jasmine";
import { SpectrumCommand } from "../src/Spectrum/components/api/decorators/spectrum-command.decorator";

describe("Spectrum Commands", () => {
TestShared.commonSetUp();

let commands: SpectrumCommands;
beforeEach(() => {
commands = TestInstance.bot.getState().commands;
});

describe("Service", () => {
it("Should expose the command service", async () => {
expect(commands).toBeTruthy();
expect(commands instanceof SpectrumCommands).toBe(true);
});

it("Should register commands through glob", async () => {
await commands.registerCommands({ commands: ["spec/mock/commands/*.ts"] });

const registered = commands.getCommandList();

expect(registered.find(command => command.shortCode === "testCommand1")).toBeTruthy();
expect(registered.find(command => command.shortCode === "testCommand2")).toBeTruthy();
});

it("Should register commands through array", async () => {
const test3 = class {
callback = () => {};
};
SpectrumCommand("test3")(test3);

const test4 = class {
callback = () => {};
};
SpectrumCommand("test4")(test4);
await commands.registerCommands({ commands: [test3, test4] });

const registered = commands.getCommandList();

expect(registered.find(command => command.shortCode === "test3")).toBeTruthy();
expect(registered.find(command => command.shortCode === "test4")).toBeTruthy();
});

it("Should not register commands that are not decorated", async () => {
const test = class {
callback = () => {};
};

await commands.registerCommands({ commands: [test] });

const registered = commands.getCommandList();

expect(registered.find(command => command.shortCode === "test5")).toBeUndefined();
});

it("Should call the callback provided on", done => {
TestShared.testLobbyDependentSetUp();
TestInstance.lobby = TestInstance.bot
.getState()
.getCommunityByName(TestShared.config._testCommunity)
.getLobbyByName(TestShared.config._testLobby);
commands.setPrefix("[UNIT]");

const testCallback = class {
callback = () => {
TestInstance.lobby.unSubscribe();
done();
};
};
SpectrumCommand("test callback")(testCallback);

commands.getCommandList();

TestInstance.lobby.subscribe();
TestInstance.lobby.sendPlainTextMessage("[UNIT] test callback");
});
});

describe("Decorator", () => {
it("Should decorate the class", () => {
const test = class {
callback = () => {};
};
SpectrumCommand("test")(test);
expect(Reflect.getMetadata("spectrum-command", test)).toBe(true);
});
});
});
1 change: 0 additions & 1 deletion spec/lobby.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ describe("Text Lobby", () => {
});

it("Should be able to subscribe to a lobby", () => {
expect(TestInstance.lobby.isSubscribed()).toBe(false);
TestInstance.lobby.subscribe();
expect(TestInstance.lobby.isSubscribed()).toBe(true);
});
Expand Down
8 changes: 8 additions & 0 deletions spec/mock/commands/command1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SpectrumCommand } from "../../../src/Spectrum/components/api/decorators/spectrum-command.decorator";

@SpectrumCommand("testCommand1")
export class TestCommand1 {
public callback() {

}
}
8 changes: 8 additions & 0 deletions spec/mock/commands/command2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SpectrumCommand } from "../../../src/Spectrum/components/api/decorators/spectrum-command.decorator";

@SpectrumCommand("testCommand2")
export class TestCommand2 {
public callback() {

}
}
7 changes: 3 additions & 4 deletions src/Spectrum/components/api/command.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
* @module Spectrum
*/ /** */

import { ISpectrumCommunity } from "../../interfaces/spectrum/community/community.interface";
import { ISpectrumLobby } from "../../interfaces/spectrum/community/chat/lobby.interface";
import { aSpectrumCommand } from "../../interfaces/api/command.interface";
import { SpectrumLobby } from "../chat/lobby.component";

/**
* #
* Helper class for a Bot Command
*
* This class is internal and should not be used anymore to create your own commands.
* Please see SpectrumCommand decorator instead
*
* @class aBotCommand
*/
export class aBotCommand implements aSpectrumCommand {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @module Spectrum
*/ /** */
import { Container } from "typedi";
import { aSpectrumCommand } from "../../../interfaces/api/command.interface";
import { SpectrumCommands } from "../../../services/commands.service";

/**
* Decorate a class as being a command a bot can listen to
*
* **example:**
*```typescript
* @SpectrumCommand("my-command")
* export class MyCommand {
* public async callback(textMessage, lobby) {
* // This will be called automatically when someone says "my-command"
* }
*}
* ```
*
* Do **not** forget to register your command via SpectrumCommands.registerCommands()
*/
export function SpectrumCommand(opts: SpectrumCommandOpts): SpectrumCommandHandlerClass;
export function SpectrumCommand(
shortCode: SpectrumCommandOpts["shortCode"]
): SpectrumCommandHandlerClass;
export function SpectrumCommand(
opts: SpectrumCommandOpts | SpectrumCommandOpts["shortCode"]
): SpectrumCommandHandlerClass {
let options: SpectrumCommandOpts;
if (typeof opts === "string" || opts instanceof RegExp) {
options = { shortCode: opts };
} else {
options = { ...opts };
}

return (
Class: new (...any: any[]) => {
callback: aSpectrumCommand["callback"];
}
) => {
Reflect.defineMetadata("spectrum-command", true, Class);
const instance = new Class();
Container.get(SpectrumCommands).registerCommand(
options.name || "",
options.shortCode,
instance.callback.bind(instance),
options.manual || ""
);
};
}

export type SpectrumCommandHandlerClass = (target: SpectrumCommandMakable) => void;

export type SpectrumCommandMakable = new (...any: any[]) => {
callback: aSpectrumCommand["callback"];
};

/**
* Options for constructing a spectrum bot command
*/
export interface SpectrumCommandOpts {
/**
* the code that will trigger this command, excluding the prefix.
* In case of a regex with capture groups, the resulting matches will be provided to the callback function
*
* **Example:**
* *(prefix set to !bot)*
*
* shortCode: `testCommand`
* Will match `!bot testCommand`
**/
shortCode: string | RegExp;
/** pretty name to be given internally to this command */
name?: string;
/** manual entry for this command */
manual?: string;
}
3 changes: 3 additions & 0 deletions src/Spectrum/components/chat/lobby.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ export class SpectrumLobby {
};
}

/**
* not implemented yet
*/
public unSubscribe() {}

/**
Expand Down
69 changes: 55 additions & 14 deletions src/Spectrum/services/commands.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { SpectrumLobby } from "../components/chat/lobby.component";
import { receivedTextMessage } from "../interfaces/spectrum/community/chat/receivedTextMessage.interface";
import { Service } from "typedi";
import { TSMap } from "typescript-map";

import { SpectrumCommandMakable } from "../components/api/decorators/spectrum-command.decorator";
import * as requireGlob from "require-glob";
import "reflect-metadata";
/** @class SpectrumCommand */
@Service()
export class SpectrumCommands {
Expand All @@ -21,7 +23,7 @@ export class SpectrumCommands {
/** The prefix for the commands */
protected prefix: string = "\\spbot";
/** Map of commands */
protected _commandMap: TSMap<string, aSpectrumCommand> = new TSMap<string, aSpectrumCommand>();
protected _commandMap: Map<string, aSpectrumCommand> = new Map<string, aSpectrumCommand>();

constructor() {
this.Broadcaster.addListener("message.new", this.checkForCommand);
Expand Down Expand Up @@ -57,16 +59,20 @@ export class SpectrumCommands {
// }
// }

this._commandMap.forEach((value: aSpectrumCommand, key: string) => {
let re = new RegExp("^" + key);
for (let [key, value] of this._commandMap.entries()) {
let re = new RegExp(`^${this.escapeRegExp(this.prefix)} ${key}`);
let matches = messageAsLower.match(re);
if (matches) {
value.callback(payload.message, lobby, matches);
return; // there cant be 2 commands can there?
}
});
}
};

/**
* Set the prefix for every commands. Any text message not starting with this prefix will be ignored
* (case insensitive)
*/
public setPrefix(prefix: string) {
this.prefix = prefix.toLowerCase();
}
Expand All @@ -79,15 +85,16 @@ export class SpectrumCommands {
* @param shortCode the shortcode to listen for
* @param callback the function to call when this command is used
* @param manual an explanation of what this command does.
* @deprecated use registerCommands instead
* @return the aSpectrumCommand object that we are now listening for.
*/
public registerCommand(command: aSpectrumCommand): aSpectrumCommand;
public registerCommand(name: string, shortCode, callback, manual): aSpectrumCommand;
public registerCommand(
name: string | aSpectrumCommand,
shortCode?,
callback?,
manual?
shortCode?: string,
callback?: Function,
manual?: string
): aSpectrumCommand {
var co = null;
if (typeof name === typeof "test") {
Expand All @@ -96,33 +103,67 @@ export class SpectrumCommands {
co = name;
}

var commandString = this.prefix.toLowerCase() + " " + co.shortCode.toLowerCase();
var commandString = co.shortCode.toLowerCase();
this._commandMap.set(commandString, co);

return co;
}

/**
* Alias of registerCommand
* @deprecated use registerCommands instead
*/
public addCommand(name, shortCode, callback, manual) {
return this.registerCommand(name, shortCode, callback, manual);
}

/**
* Unbinds a command and stop listening to it.
* @todo
*/
public unRegisterCommand(command: aBotCommand);
public unRegisterCommand(commandId: number);
public unRegisterCommand(co) {
let shortcodeAsLower = co.shortCode.toLowerCase();

this._commandMap.filter(function(command, key) {
return key === shortcodeAsLower;
});
}

/**
* Return the list of commands currently registered and active
*/
public getCommandList(): aSpectrumCommand[] {
return this._commandMap.values();
return Array.from(this._commandMap.values());
}

/**
* Register a batch of commands, either as an array or with a glob.
* Commands __must be decorated__ with @SpectrumCommand() decorator
*/
public async registerCommands(opts: {
/** array of actual commands or globs to import them */
commands: SpectrumCommandMakable[] | string[];
}) {
if (opts.commands.length === 0) return;

if (typeof opts.commands[0] === "string") {
// Import as glob, we have nothing to do.
await requireGlob((opts.commands as string[]).map(path => `${process.cwd()}/${path}`));
} else {
await Promise.all(
(opts.commands as SpectrumCommandMakable[]).map(async Command => {
// We just make sure it's decorated and that we have nothing to do.
if (!Reflect.getMetadata("spectrum-command", Command)) {
console.error(
`[ERROR] Could not register command ${
Command.constructor.name
}. Did you forget to decorate it with @SpectrumCommand() ?`
);
}
})
);
}
}

protected escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
}
Loading

0 comments on commit a866dfc

Please sign in to comment.