twitch2ma
Version:
Twitch chat bot that runs commands on the MA GrandMA2 using Telnet.
314 lines • 14.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChannelError = exports.TelnetError = void 0;
const typed_event_emitter_1 = require("@d-fischer/typed-event-emitter");
const twitch_1 = require("twitch");
const twitch_chat_client_1 = require("twitch-chat-client");
const Config_1 = require("./Config");
const RuntimeInformation_1 = require("./RuntimeInformation");
const PermissionController_1 = require("./PermissionController");
const SACNPermission_1 = require("./permissions/SACNPermission");
const CooldownPermission_1 = require("./permissions/CooldownPermission");
const OwnerPermission_1 = require("./permissions/OwnerPermission");
const ModeratorPermission_1 = require("./permissions/ModeratorPermission");
const sentry_1 = require("./sentry");
const Sentry = require("@sentry/node");
const Bluebird = require("bluebird");
global.Promise = Bluebird;
const SourceMapSupport = require("source-map-support");
const _ = require("lodash");
const TelnetClient = require("telnet-client");
SourceMapSupport.install();
class TelnetError extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, TelnetError.prototype);
this.name = TelnetError.name;
}
}
exports.TelnetError = TelnetError;
class ChannelError extends Error {
constructor() {
super("Joining channel failed. Did you type the channel name correctly?");
Object.setPrototypeOf(this, ChannelError.prototype);
this.name = ChannelError.name;
}
}
exports.ChannelError = ChannelError;
class Twitch2Ma extends typed_event_emitter_1.EventEmitter {
constructor(config) {
super();
this.onTelnetConnected = this.registerEvent();
this.onTwitchConnected = this.registerEvent();
this.onError = this.registerEvent();
this.onCommandExecuted = this.registerEvent();
this.onHelpExecuted = this.registerEvent();
this.onPermissionDenied = this.registerEvent();
this.onGodMode = this.registerEvent();
this.onNotice = this.registerEvent();
this.onConfigReloaded = this.registerEvent();
this.config = config;
this.telnet = new TelnetClient();
this.initializing = false;
this.permissionController = new PermissionController_1.PermissionController()
.withPermissionInstance(new SACNPermission_1.default(config)
.withOnStatusHandler(status => this.handleSACNStatus(status))
.withOnErrorHandler(error => this.handleSACNError(error)))
.withPermissionInstance(new CooldownPermission_1.default())
.withPermissionInstance(new OwnerPermission_1.default())
.withPermissionInstance(new ModeratorPermission_1.default());
}
reloadConfig(config) {
if (this.initializing) {
throw new Error("twitch2ma is initializing!");
}
let needsTelnetReload = !_.isEqual(this.config.ma, config.ma);
let needsTwitchReload = !_.isEqual(this.config.twitch, config.twitch) || needsTelnetReload;
this.initializing = true;
let reloadChain = Promise.resolve();
if (needsTwitchReload) {
reloadChain = reloadChain
.then(() => this.emit(this.onNotice, "Disconnecting Twitch..."))
.then(() => this.chatClient.removeListener())
.then(() => this.stopTwitch());
}
if (needsTelnetReload) {
reloadChain = reloadChain
.then(() => this.emit(this.onNotice, "Reconnecting telnet..."))
.then(() => this.telnet.end())
.then(() => this.startTelnet(config.ma.host, config.ma.user, config.ma.password));
}
if (needsTwitchReload) {
reloadChain = reloadChain
.then(() => this.emit(this.onNotice, "Connecting Twitch..."))
.then(() => this.initTwitch(config));
}
return reloadChain
.then(() => this.permissionController.reloadConfig(config))
.then(() => this.config = config)
.then(() => this.initializing = false)
.then(() => this.emit(this.onConfigReloaded))
.catch(error => sentry_1.default(error, () => this.stopWithError(error)));
}
startTelnet(host, user, password) {
return this.telnet
.connect({
host: host,
port: 30000,
shellPrompt: /\[.+]>.../,
echoLines: 0,
ors: "\r\n",
})
.catch(() => {
throw new TelnetError("Could not connect to desk! Check Telnet enabled, MA IP address and firewall " +
"settings if using onPC!");
})
.then(() => this.telnetLogin(host, user, password));
}
stopTwitch() {
let stopChain = Promise.resolve();
if (_.isObject(this.chatClient)) {
stopChain.then(() => this.chatClient.quit());
}
return stopChain;
}
start() {
this.initializing = true;
return this.permissionController.start()
.then(() => this.startTelnet(this.config.ma.host, this.config.ma.user, this.config.ma.password))
.then(() => this.initTwitch())
.then(() => this.initializing = false);
}
stop() {
return this.stopTwitch()
.then(() => this.telnet.end())
.then(() => this.permissionController.stop());
}
stopWithError(error) {
this.emit(this.onError, error);
return this.stop();
}
telnetLogin(host, user, password) {
return this.telnet.exec(`Login ${user} ${password}`)
.then((message) => {
if (message.match(`Logged in as User '${user}'`)) {
this.emit(this.onTelnetConnected, host, user);
}
else {
throw new TelnetError(`Could not log into the desk as user ${user}! Check password!`);
}
});
}
initTwitch(config = this.config) {
return Promise.resolve()
.then(() => new Promise((resolve, reject) => {
this.twitchClient = twitch_1.default.withCredentials(config.twitch.clientId, config.twitch.accessToken);
this.chatClient = twitch_chat_client_1.default.forTwitchClient(this.twitchClient, {
logger: {
minLevel: "CRITICAL"
}
});
this.chatClient.onPrivmsg((channel, user, message, rawMessage) => this.handleMessage(channel, user, message, rawMessage));
this.chatClient.onRegister(() => {
this.chatClient.join(config.twitch.channel)
.then(() => resolve())
.catch(() => reject(new ChannelError()));
});
this.chatClient.connect()
.catch(error => reject(error));
})).then(() => this.emit(this.onTwitchConnected, config.twitch.channel));
}
handleMessage(channel, user, message, rawMessage) {
return __awaiter(this, void 0, void 0, function* () {
if (this.initializing) {
this.chatClient.say(channel, "The bot is currently reloading its config, please wait a moment " +
"and try it again!");
this.emit(this.onNotice, `${channel}: ${user} tried to execute something but it was denied due to reloading config.`);
return;
}
let raw = message.match(/!([a-zA-Z0-9-]+)( !?([a-zA-Z0-9-]+))?/);
if (_.isArray(raw)) {
let chatCommand = raw[1];
let parameterName = raw[3];
if (chatCommand === "lights") {
this.sendHelp(channel, user, parameterName);
}
else {
let command = this.config.getCommand(chatCommand);
if (command instanceof Config_1.Command) {
let instructions = command;
if (_.isString(parameterName)) {
let parameter = command.getParameter(parameterName);
if (parameter instanceof Config_1.Parameter) {
instructions = parameter;
}
else {
this.chatClient.say(channel, `Parameter ${parameterName} does not exist! Type !lights !${chatCommand} for help!`);
return;
}
}
return this.sendCommand(instructions, channel, user, rawMessage)
.then(() => {
if (_.isString(instructions.message)) {
this.chatClient.say(channel, instructions.message
.replace("{user}", `@${user}`)
.replace("{parameterList}", this.getParametersHelp(command))
.trim());
}
})
.then(() => this.emit(this.onCommandExecuted, channel, user, chatCommand, parameterName, instructions.consoleCommand))
.catch(PermissionController_1.PermissionError, error => {
let command = _.isString(parameterName) ? `!${chatCommand} ${parameterName}` : `!${chatCommand}`;
let reason = error.permissionCollector.permissionDeniedReasons.shift();
this.chatClient.say(channel, reason.viewerMessage.replace("{command}", command));
this.emit(this.onPermissionDenied, channel, user, command, reason.name);
})
.catch(TelnetError, error => this.stopWithError(error))
.catch(error => sentry_1.default(error, error => this.stopWithError(error)));
}
}
}
});
}
sendCommand(instructions, channel, user, rawMessage) {
return __awaiter(this, void 0, void 0, function* () {
return this.permissionController.checkPermissions(new RuntimeInformation_1.RuntimeInformation(this.config, user, rawMessage, instructions))
.then((permissionCollector) => {
if (permissionCollector.permissionDeniedReasons.length > 0 && permissionCollector.godMode) {
this.emit(this.onGodMode, channel, user, permissionCollector.godModeReasons.shift());
}
})
.then(() => {
if (_.isString(instructions.consoleCommand)) {
return this.telnet.send(instructions.consoleCommand)
.catch(() => {
throw new TelnetError("Sending telnet command failed! Is MA still running?");
})
.then(() => this.permissionController.setAdditionalRuntimeInformation("lastCall", new Date().getTime()));
}
});
});
}
sendHelp(channel, user, helpCommand) {
let message;
if (_.isString(helpCommand)) {
let command = this.config.getCommand(helpCommand);
if (command instanceof Config_1.Command) {
if (_.isString(command.help)) {
message = `Help for !${helpCommand}: ${command.help.replace("{parameterList}", this.getParametersHelp(command)).trim()}`;
}
else {
message = `No help for !${helpCommand} available!`;
}
this.emit(this.onHelpExecuted, channel, user, command.chatCommand);
}
else {
message = `Command !${helpCommand} does not exist! Type !lights for a list of available commands.`;
}
}
else {
if (_.size(this.config.commands)) {
message = "Available commands are: " + this.config.availableCommands + ". Type !lights !command for help.";
}
else {
message = "There are no commands available.";
}
this.emit(this.onHelpExecuted, channel, user);
}
this.chatClient.say(channel, message);
}
getParametersHelp(command) {
return _.isString(command.availableParameters) ? `Available parameters: ${command.availableParameters}` : "";
}
handleSACNStatus(status) {
switch (status.constructor) {
case SACNPermission_1.SACNWaiting:
this.emit(this.onNotice, `sACN status: Waiting for universes: ${status.universes.join(", ")}`);
break;
case SACNPermission_1.SACNReceiving:
this.emit(this.onNotice, `sACN status: Receiving universes: ${status.universes.join(", ")}`);
break;
case SACNPermission_1.SACNLost:
this.emit(this.onNotice, `sACN status: Lost universes: ${status.universes.join(", ")}`);
break;
case SACNPermission_1.SACNStopped:
this.emit(this.onNotice, "sACN status: Stopped listening.");
break;
default:
this.emit(this.onNotice, `sACN status: Received unknown status: ${typeof status}`);
sentry_1.sentryMessage(`Unknown sACN status received: ${typeof status}`, Sentry.Severity.Warning);
break;
}
}
handleSACNError(error) {
switch (error.constructor) {
case SACNPermission_1.SACNError:
this.emit(this.onNotice, error.message);
break;
default:
// @ts-ignore
this.emit(this.onNotice, "sACN error: unknown error" + (error.code ? ` (${error.code})` : ""));
sentry_1.default(error);
break;
}
}
emit(event, ...args) {
try {
super.emit(event, ...args);
}
catch (error) {
sentry_1.default(error);
}
}
}
exports.default = Twitch2Ma;
//# sourceMappingURL=Twitch2Ma.js.map