twitch2ma
Version:
Twitch chat bot that runs commands on the MA GrandMA2 using Telnet.
379 lines (253 loc) • 14.6 kB
text/typescript
import {TelnetError, ChannelError} from "../src/lib/Twitch2Ma";
import Twitch2Ma from "../src/lib/Twitch2Ma";
import {Config} from "../src/lib/Config";
import TwitchPrivateMessage from "twitch-chat-client/lib/StandardCommands/TwitchPrivateMessage";
import TwitchChatClient from "twitch-chat-client";
import Fs = require("fs");
import _ = require("lodash");
// Workaround, see https://github.com/evanw/node-source-map-support/issues/279
jest.mock("source-map-support");
////
jest.mock("twitch-chat-client");
jest.mock("telnet-client", require("./mocks/telnet-client"));
let config: Config;
let twitch2Ma: Twitch2Ma;
beforeEach(() => {
config = loadConfig();
twitch2Ma = getTwitch2MaInstanceAndEnableLogin();
});
afterEach(() => {
twitch2Ma.stop();
jest.clearAllMocks();
});
test("Connection chain", async () => {
let spyOnTelnetLogin = jest.spyOn(twitch2Ma, "telnetLogin");
let spyOnInitTwitch = jest.spyOn(twitch2Ma, "initTwitch");
let spyOnOnError = jest.spyOn(twitch2Ma, "onError");
let spyOnOnTelnetConnected = jest.fn();
let spyOnOnTwitchConnected = jest.fn();
twitch2Ma.onTelnetConnected(spyOnOnTelnetConnected);
twitch2Ma.onTwitchConnected(spyOnOnTwitchConnected);
await expect(twitch2Ma.start()).resolves.toBe(false);
expect(twitch2Ma["telnet"].connect).toBeCalled();
expect(spyOnTelnetLogin).toBeCalled();
expect(spyOnInitTwitch).toBeCalled();
expect(twitch2Ma["chatClient"].join).toBeCalled();
expect(spyOnOnTelnetConnected).toBeCalledWith(config.ma.host, config.ma.user);
expect(spyOnOnTwitchConnected).toBeCalledWith(config.twitch.channel);
expect(spyOnOnError).not.toBeCalled();
});
test("Telnet connection failed", async () => {
jest.spyOn(twitch2Ma["telnet"], "connect").mockRejectedValueOnce("Fooled!");
let spyOnOnTelnetConnected = jest.fn();
let spyOnOnTwitchConnected = jest.fn();
twitch2Ma.onTelnetConnected(spyOnOnTelnetConnected);
twitch2Ma.onTwitchConnected(spyOnOnTwitchConnected);
await expect(twitch2Ma.start()).rejects.toThrow(new TelnetError("Could not connect to desk! Check Telnet enabled, " +
"MA IP address and firewall settings if using onPC!"));
expect(spyOnOnTelnetConnected).not.toBeCalled();
expect(spyOnOnTwitchConnected).not.toBeCalled();
});
test("Telnet login failed", async () => {
await twitch2Ma.stop();
twitch2Ma = new Twitch2Ma(config);
let spyOnOnTelnetConnected = jest.fn();
let spyOnOnTwitchConnected = jest.fn();
twitch2Ma.onTelnetConnected(spyOnOnTelnetConnected);
twitch2Ma.onTwitchConnected(spyOnOnTwitchConnected);
await expect(twitch2Ma.start()).rejects
.toThrow(new TelnetError(`Could not log into the desk as user ${config.ma.user}! Check password!`));
expect(spyOnOnTelnetConnected).not.toBeCalled();
expect(spyOnOnTwitchConnected).not.toBeCalled();
});
test("Twitch connection failed", async () => {
let spyOnOnTelnetConnected = jest.fn();
let spyOnOnTwitchConnected = jest.fn();
twitch2Ma.onTelnetConnected(spyOnOnTelnetConnected);
twitch2Ma.onTwitchConnected(spyOnOnTwitchConnected);
jest.spyOn(TwitchChatClient, "forTwitchClient")
.mockImplementationOnce(() => {
throw Error("Not this time!");
});
await expect(twitch2Ma.start()).rejects.toMatchObject(new Error("Not this time!"));
expect(spyOnOnTelnetConnected).toBeCalledWith(config.ma.host, config.ma.user);
expect(spyOnOnTwitchConnected).not.toBeCalled();
});
test("Twitch channel join failed", async () => {
// @ts-ignore
TwitchChatClient.joinImplementation.mockImplementationOnce(() => {
return () => {
return new Promise((resolve, reject) => reject());
}
});
let errorHandler = jest.fn();
let spyOnOnTelnetConnected = jest.fn();
let spyOnOnTwitchConnected = jest.fn();
let spyOnError = jest.fn();
twitch2Ma.onTelnetConnected(spyOnOnTelnetConnected);
twitch2Ma.onTwitchConnected(spyOnOnTwitchConnected);
twitch2Ma.onError(spyOnError);
await expect(twitch2Ma.start()).rejects.toThrow(new ChannelError());
expect(twitch2Ma["chatClient"].join).toBeCalled();
expect(spyOnOnTelnetConnected).toBeCalledWith(config.ma.host, config.ma.user);
expect(spyOnOnTwitchConnected).not.toBeCalled();
expect(spyOnError).not.toBeCalled();
})
test("Message handler set", async () => {
await twitch2Ma.start();
let spyOnMessageHandler = jest.spyOn(twitch2Ma, "handleMessage");
await sendMessageToBot(twitch2Ma, "#mychannel", "myUser", "myMessage", null);
expect(spyOnMessageHandler).toBeCalled();
});
test("Send help", async () => {
let helpExecutedHandler = jest.fn();
twitch2Ma.onHelpExecuted(helpExecutedHandler);
await twitch2Ma.start();
let spyOnTwitchSay = jest.spyOn(twitch2Ma["chatClient"], "say");
await sendMessageToBotAndExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter", "Alice", "!lights", null,
`Available commands are: ${config.availableCommands}. Type !lights !command for help.`);
expect(helpExecutedHandler).toBeCalledWith("#doesNotMatter", "Alice");
helpExecutedHandler.mockReset();
await sendMessageToBotAndExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter", "Bob", "!lights !doesNotExist", null,
"Command !doesNotExist does not exist! Type !lights for a list of available commands.");
expect(helpExecutedHandler).not.toBeCalled();
await sendMessageToBotAndExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter", "Celine", "!lights !red", null,
"Help for !red: Sets the lights to red");
expect(helpExecutedHandler).toBeCalledWith("#doesNotMatter", "Celine", "red");
await sendMessageToBotAndExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter", "Celine", "!lights blue", null,
"Help for !blue: Sets the lights to blue");
expect(helpExecutedHandler).toBeCalledWith("#doesNotMatter", "Celine", "blue");
await sendMessageToBotAndExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter", "Mary", "!lights !yellow", null,
"No help for !yellow available!");
expect(helpExecutedHandler).toBeCalledWith("#doesNotMatter", "Mary", "yellow");
await sendMessageToBotAndExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter", "Noely", "!lights !gobo", null,
"Help for !gobo: Gobo commands. Available parameters: niceGobo, evenNicerGobo");
expect(helpExecutedHandler).toBeCalledWith("#doesNotMatter", "Noely", "gobo");
});
test("Send help w/o commands", async () => {
let helpExecutedHandler = jest.fn();
await twitch2Ma.stop();
twitch2Ma = getTwitch2MaInstanceAndEnableLogin(loadConfig({commands: []}));
twitch2Ma.onHelpExecuted(helpExecutedHandler);
await twitch2Ma.start();
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!lights", null, "There are no commands available.");
expect(helpExecutedHandler).toBeCalledWith("#doesNotMatter", "Alice");
});
test("Command successful", async () => {
let aliceRawMessage = new TwitchPrivateMessage("doesNotMatter", null, null, {nick: "Alice"});
let commandExecutedHandler = jest.fn();
twitch2Ma.onCommandExecuted(commandExecutedHandler);
await twitch2Ma.start();
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!red", aliceRawMessage, "@Alice set the lights to red!");
expect(twitch2Ma["telnet"].send).toBeCalledWith("Macro 1");
expect(commandExecutedHandler).toBeCalledWith("#doesNotMatter", "Alice", "red", undefined, "Macro 1");
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!red", aliceRawMessage, "@Alice, please wait \\d{1,2} seconds and try again!");
});
test("Text only command successful", async () => {
let aliceRawMessage = new TwitchPrivateMessage("doesNotMatter", null, null, {nick: "Alice"});
let commandExecutedHandler = jest.fn();
twitch2Ma.onCommandExecuted(commandExecutedHandler);
await twitch2Ma.start();
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!textOnly", aliceRawMessage, "Heyyy @Alice!");
expect(twitch2Ma["telnet"].send).not.toBeCalled();
expect(commandExecutedHandler).toBeCalledWith("#doesNotMatter", "Alice", "textOnly", undefined, undefined);
});
test("Parameter successful", async () => {
let aliceRawMessage = new TwitchPrivateMessage("doesNotMatter", null, null, {nick: "Alice"});
let commandExecutedHandler = jest.fn();
twitch2Ma.onCommandExecuted(commandExecutedHandler);
await twitch2Ma.start();
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!gobo evenNicerGobo", aliceRawMessage, "@Alice wishes the 'evenNicerGobo'!");
expect(twitch2Ma["telnet"].send).toBeCalledWith("Macro 5");
expect(commandExecutedHandler).toBeCalledWith("#doesNotMatter", "Alice", "gobo", "evenNicerGobo", "Macro 5");
});
test("Parameter does not exist", async () => {
let aliceRawMessage = new TwitchPrivateMessage("doesNotMatter", null, null, {nick: "Alice"});
let commandExecutedHandler = jest.fn();
twitch2Ma.onCommandExecuted(commandExecutedHandler);
await twitch2Ma.start();
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!gobo doesNotExist", aliceRawMessage, "Parameter doesNotExist does not exist! Type !lights !gobo for help!");
expect(twitch2Ma["telnet"].send).not.toBeCalled();
});
test("Telnet command failed", async () => {
let aliceRawMessage = new TwitchPrivateMessage("doesNotMatter", null, null, {nick: "Alice"});
let commandExecutedHandler = jest.fn();
let errorHandler = jest.fn();
twitch2Ma.onCommandExecuted(commandExecutedHandler);
twitch2Ma.onError(errorHandler);
jest.spyOn(twitch2Ma["telnet"], "send").mockRejectedValueOnce(new Error());
await twitch2Ma.start();
await sendMessageToBotAndDontExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!red", aliceRawMessage);
expect(twitch2Ma["telnet"].send).toBeCalledWith("Macro 1");
expect(errorHandler).toBeCalledWith(new TelnetError("Sending telnet command failed! Is MA still running?"));
expect(commandExecutedHandler).not.toBeCalled();
});
test("Command permissions denied", async () => {
let aliceRawMessage = new TwitchPrivateMessage("doesNotMatter", null, null, {nick: "Alice"});
let permissionDeniedHandler = jest.fn();
twitch2Ma.onPermissionDenied(permissionDeniedHandler);
await twitch2Ma.start();
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!red", aliceRawMessage, "@Alice set the lights to red!");
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!red", aliceRawMessage, "@Alice, please wait \\d{1,2} seconds and try again!");
return expect(permissionDeniedHandler).toBeCalledWith("#doesNotMatter", "Alice", "!red", "cooldown");
});
test("Command permissions denied but godMode enabled", async () => {
let aliceRawMessage = new TwitchPrivateMessage("doesNotMatter", null, new Map([["badges", "moderator"]]), {nick: "Alice"});
let godModeHandler = jest.fn();
twitch2Ma.onGodMode(godModeHandler);
await twitch2Ma.start();
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!red", aliceRawMessage, "@Alice set the lights to red!");
await sendMessageToBotAndExpectAnswer(twitch2Ma, jest.spyOn(twitch2Ma["chatClient"], "say"), "#doesNotMatter",
"Alice", "!red", aliceRawMessage, "@Alice set the lights to red!");
return expect(godModeHandler).toBeCalledWith("#doesNotMatter", "Alice", "user is moderator");
})
test("Message not involving the bot", async () => {
await twitch2Ma.start();
let spyOnTwitchSay = jest.spyOn(twitch2Ma["chatClient"], "say");
await sendMessageToBotAndDontExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter",
"Alice", "Hello world!", null);
await sendMessageToBotAndDontExpectAnswer(twitch2Ma, spyOnTwitchSay, "#doesNotMatter",
"Alice", "!command", null);
});
async function sendMessageToBotAndExpectAnswer(twitch2Ma: Twitch2Ma,
spyOnTwitchSay: jest.SpyInstance,
channel: string,
user: string,
message: string,
rawMessage: TwitchPrivateMessage,
answer: string) {
await sendMessageToBot(twitch2Ma, channel, user, message, rawMessage);
expect(spyOnTwitchSay).toBeCalledWith(channel, expect.stringMatching(answer));
}
async function sendMessageToBotAndDontExpectAnswer(twitch2Ma: Twitch2Ma,
spyOnTwitchSay: jest.SpyInstance,
channel: string,
user: string,
message: string,
rawMessage: TwitchPrivateMessage) {
await sendMessageToBot(twitch2Ma, channel, user, message, rawMessage);
expect(spyOnTwitchSay).not.toBeCalled();
}
async function sendMessageToBot(twitch2Ma: Twitch2Ma, channel: string, user: string, message: string, rawMessage: TwitchPrivateMessage) {
// @ts-ignore
await twitch2Ma["chatClient"].onPrivmsgHandler(channel, user, message, rawMessage);
}
function getTwitch2MaInstanceAndEnableLogin(newConfig?: Config): Twitch2Ma {
let twitch2Ma = new Twitch2Ma(newConfig || config);
jest.spyOn(twitch2Ma["telnet"], "exec")
.mockResolvedValueOnce(`Logged in as User '${config.ma.user}'`);
return twitch2Ma;
}
function loadConfig(overrideConfigValues?: any): Config {
return new Config(_.assign({}, JSON.parse(Fs.readFileSync("config.json.sample", {encoding: "utf-8"})), overrideConfigValues));
}