twitch2ma
Version:
Twitch chat bot that runs commands on the MA GrandMA2 using Telnet.
273 lines (213 loc) โข 9.16 kB
text/typescript
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);
}