UNPKG

twitch2ma

Version:

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

273 lines (213 loc) โ€ข 9.16 kB
import {Command} from "commander"; import {HTTPStatusCodeError, InvalidTokenError} from "twitch"; import Twitch2Ma, {TelnetError} from "./Twitch2Ma"; import {Config, ConfigError} from "./Config"; import {CommandSocket, SocketError} from "./CommandSocket"; import {Logger} from "./Logger"; import sentry from "./sentry"; import axios from "axios"; import Fs = require("fs"); import YAML = require("yaml"); import _ = require("lodash"); import chalk = require("chalk"); import ipc = require("node-ipc"); import childProcess = require("child_process"); const semverGt = require('semver/functions/gt') const packageInformation = require("../../package.json"); // TODO Improve: let logger = new Logger(); let commandSocket: CommandSocket = new CommandSocket(); let globalTwitch2Ma: Twitch2Ma; let globalConfigFile: string; export function main() { process.on("SIGINT", () => exit(0)); axios.get("https://registry.npmjs.org/twitch2ma") .then(response => notifyUpdate(response.data["dist-tags"].latest)) .catch((error: Error) => sentry(error, () => logger.warning("Could not get update information!"))) .then(init); } function init(): void { let program = new Command() .name(packageInformation.name) .description(packageInformation.description) .version(packageInformation.version, "-v, --version", "output the current version"); program.command("start", {isDefault: true}) .description("start twitch2ma bot") .arguments("[configFile]") .option("-d, --detach", "detach the bot from the terminal and run as daemon") .option("-l, --logfile <logfile>", "log to logfile") .action((configFile, cmd) => { loadConfig(configFile) .then(config => new Twitch2Ma(config)) .then(twitch2Ma => { if (cmd.detach) { return commandSocket.checkSocketExists().then(() => startChildProcess()); } else { if (cmd.logfile) { logger.setLogfile(cmd.logfile); } globalTwitch2Ma = twitch2Ma; return attachEventHandlers(twitch2Ma) .then(twitch2Ma => twitch2Ma.start()) .then(() => openCommandSocket()); } }) // @ts-ignore .catch(ConfigError, TelnetError, SocketError, error => exitWithError(error)) .catch(InvalidTokenError, () => exitWithError(new Error("Twitch error: Access token invalid!"))) .catch(HTTPStatusCodeError, error => { if (error.statusCode === 500) { return exitWithError(new Error("Twitch error: Twitch seems to be broken at the moment (see " + "https://status.twitch.tv for status) ๐Ÿ˜•")); } else { throw error; } }) .catch(error => sentry(error, exitWithError)); }); program.command("stop") .description("stop twitch2ma bot") .action(() => emitSocketEvent("exit")); program.command("reloadConfig [configFile]") .description("reload running twitch2ma instance with a new config") .action(configFile => emitSocketEvent("reloadConfig", configFile)); program.parse(process.argv); } function startChildProcess() { for (let i = 0; i < process.argv.length; i++) { if (process.argv[i] === "-d" || process.argv[i] === "--detach") { process.argv[i] = ""; } } process.argv[0] = ""; let subProcess = childProcess.spawn(process.argv0, process.argv, { stdio: "ignore", shell: true, detached: true, windowsHide: true }); if (subProcess) { subProcess.unref(); logger.confirm("twitch2ma starting detached!"); } else { throw new Error("twitch2ma could not start detached!"); } } function emitSocketEvent(event: string, payload?: any) { ipc.config.appspace = "twitch2ma."; ipc.config.silent = true; ipc.connectTo("main", () => { ipc.of.main.on("connect", () => { ipc.of.main.emit(event, payload); ipc.disconnect("main"); }); ipc.of.main.on("error", () => { ipc.disconnect("main"); logger.error("Could not connect to socket: Is twitch2ma running?"); process.exit(1); }); }); } function openCommandSocket() { commandSocket.onError(error => exitWithError(error)); commandSocket.onExitCommand(() => { logger.socketMessage("Exit command received!"); exit(0); }); commandSocket.onReloadConfigCommand((newConfigFile: any) => { if(!_.isString(newConfigFile)) { newConfigFile = globalConfigFile; } logger.socketMessage(`Reload config command received (${newConfigFile})!`); loadConfig(newConfigFile) .then(config => globalTwitch2Ma.reloadConfig(config)) .catch(error => sentry(error, () => logger.error(`Reloading config failed: ${error.message}`))); }); return commandSocket.start(); } export function notifyUpdate(latestVersion: string) { if (semverGt(latestVersion, packageInformation.version)) { console.log(chalk`๐Ÿ”” {blue A new version of ${packageInformation.name} is available!} ` + chalk`{blue (current: {bold ${packageInformation.version}}, new: {bold ${latestVersion}})}`); } } export async function attachEventHandlers(twitch2Ma: Twitch2Ma): Promise<Twitch2Ma> { twitch2Ma.onTelnetConnected((host, user) => logger.confirm(chalk`Telnet connected to {bold ${user}:***@${host}:30000}.`)); twitch2Ma.onTwitchConnected(channel => logger.confirm(chalk`Twitch connected to {bold #${channel}}.`)); twitch2Ma.onCommandExecuted((channel, user, chatCommand, parameterName, consoleCommand) => { parameterName = _.isString(parameterName) ? ` ${parameterName}` : ""; logger.channelMessage(channel, chalk`๐Ÿ’ก User {bold ${user}} executed {bold.blue !${chatCommand}${parameterName}}` + (_.isString(consoleCommand) ? chalk` ({magenta ${consoleCommand}}) on the desk.` : '.')); }); twitch2Ma.onHelpExecuted((channel, user, helpCommand) => { if (_.isString(helpCommand)) { logger.channelMessage(channel, chalk`๐Ÿคจ User {bold ${user}} got help for {bold.blue !${helpCommand}}.`); } else { logger.channelMessage(channel, chalk`๐Ÿ“– User {bold ${user}} listed available commands.`); } }); twitch2Ma.onGodMode((channel, user, reason) => logger.channelMessage(channel, chalk`๐Ÿ’ช User {bold ${user}} activated {bold.inverse god mode } because: ${reason}.`)); twitch2Ma.onPermissionDenied((channel, user, command, reason) => logger.channelMessage(channel, chalk`โœ‹ User {bold ${user}} tried to run {bold.blue ${command}} but permissions were denied by ${reason}.`)) twitch2Ma.onNotice(message => logger.notice(message)); twitch2Ma.onConfigReloaded(() => logger.confirm(`Config reloaded.`)) twitch2Ma.onError(error => exitWithError(error)); return twitch2Ma; } export async function loadConfig(configFile: string): Promise<Config> { let configFileIsDefault = !_.isString(configFile); if (configFileIsDefault) { configFile = "config.json"; } let rawConfigFile: string; try { rawConfigFile = Fs.readFileSync(configFile, {encoding: "utf-8"}); } catch (error) { if (error.code === "ENOENT") { throw new ConfigError(`Could not open config file ${configFile}!`); } else { throw error; } } let rawConfigObject; try { rawConfigObject = JSON.parse(rawConfigFile); } catch (error) { try { rawConfigObject = YAML.parse(rawConfigFile); } catch (ignored) { throw new ConfigError(`Config file ${configFile} is not a valid JSON or YAML file!`); } } globalConfigFile = configFile; return new Config(rawConfigObject); } async function exit(statusCode: number) { let stopChain = Promise.resolve(); if (globalTwitch2Ma) { stopChain = stopChain .then(() => globalTwitch2Ma.stop()) .catch(err => { sentry(err); process.exit(1); }); } return stopChain .then(() => console.log(chalk`\n{bold Thank you for using twitch2ma} โค๏ธ`)) .then(() => logger.end()) .then(() => { if (commandSocket) { commandSocket.stop(); } }) .finally(() => process.exit(statusCode)); } export async function exitWithError(err: Error) { logger.error((err.message.slice(-1).match(/[!?]/) ? err.message : err.message + "!") + " Exiting..."); logger.end(); if (commandSocket) { commandSocket.stop(); } return exit(1); }