UNPKG

twitch2ma

Version:

Twitch chat bot that runs commands on the MA GrandMA2 using Telnet.

314 lines 14.9 kB
"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