UNPKG

twitch2ma

Version:

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

265 lines (202 loc) 8.29 kB
import {PermissionCollector, PermissionInstance} from "../PermissionController"; import {RuntimeInformation} from "../RuntimeInformation"; import {Config} from "../Config"; import {Packet, Receiver} from "sacn"; import {EventEmitter} from "@d-fischer/typed-event-emitter"; import {EventBinder} from "@d-fischer/typed-event-emitter/lib/EventEmitter"; import {SACNUniverse, UniverseStatus} from "./SACNUniverse"; import sentry from "../sentry"; import _ = require("lodash"); export default class SACNPermission extends EventEmitter implements PermissionInstance { private readonly universes: Array<SACNUniverse>; private config: Config; private sACNReceiver: Receiver; private watchdogTimeout: NodeJS.Timeout; private running: boolean; constructor(config: Config) { super(); this.universes = []; this.config = config; this.running = false; } check(permissionCollector: PermissionCollector, runtimeInformation: RuntimeInformation, additionalRuntimeInformation: Map<String, any>): void { if (runtimeInformation.instructions) { let sacn = runtimeInformation.instructions.sacn; if (sacn) { let universe = this.universes[sacn.universe]; if (universe && (universe.status !== UniverseStatus.Valid || universe.data[sacn.channel - 1] < 255)) { permissionCollector.denyPermission("sacn", `@${runtimeInformation.userName}, ${runtimeInformation.config.sacn.lockMessage}`); } } } } start(): Promise<void> { if (!this.running) { for (const universe of SACNPermission.getUniverses(this.config)) { this.universes[universe] = new SACNUniverse(universe); } let universes = SACNPermission.getUniverses(this.config); if (universes.length > 0) { let receiverOptions = { universes: universes, reuseAddr: true }; if (this.config.sacn && _.isString(this.config.sacn.interface)) { _.set(receiverOptions, "iface", this.config.sacn.interface); } this.sACNReceiver = new Receiver(receiverOptions); this.sACNReceiver.on("packet", (packet: Packet) => { this.universes[packet.universe].data = [...packet.payloadAsBuffer]; }); this.sACNReceiver.on("PacketCorruption", error => { this.emit(this.onError, new SACNCorruptError()); this.stop().catch(error => sentry(error)); sentry(error); }); this.sACNReceiver.on("error", (error: Error) => { // @ts-ignore if (error.code) { // @ts-ignore switch (error.code) { case "EADDRNOTAVAIL": case "ENODEV": this.emit(this.onError, new SACNError(`sACN error: Have you configured the right interface IP?`)); break; case "ERR_SOCKET_DGRAM_NOT_RUNNING": this.emit(this.onError, new SACNError("sACN error: UDP subsystem not running!")); break; default: this.emit(this.onError, error); break; } } else { this.emit(this.onError, error); } this.stop().catch(error => sentry(error)); }); this.emit(this.onStatus, new SACNWaiting(universes)); this.watchdogTimeout = setInterval(() => this.watchdog(), this.config.sacn.timeout); this.watchdogTimeout.unref(); this.running = true; } } return Promise.resolve(); } stop(): Promise<any> { let stopChain: Promise<any> = Promise.resolve(); if (this.sACNReceiver && this.running) { stopChain = stopChain .then(() => new Promise(resolve => { this.sACNReceiver.close(() => { resolve(); }); })) .catch(error => sentry(error)); } _.attempt(() => clearInterval(this.watchdogTimeout)); return stopChain .then(() => this.running = false) .then(() => this.emit(this.onStatus, new SACNStopped())); } reloadConfig(config: Config): Promise<void> { if (!_.isEqual(this.config.sacn, config.sacn) || !_.isEqual(SACNPermission.getUniverses(this.config), SACNPermission.getUniverses(config))) { return this.stop() .then(() => this.config = config) .then(() => this.start()); } return Promise.resolve(); } private watchdog() { let lastValidTime = new Date().getTime() - this.config.sacn.timeout * 1000; let receiving = []; let lost = []; for (const universe of this.universes) { if (!universe) { continue; } if (lastValidTime <= universe.lastReceived && (universe.watchdogStatus == UniverseStatus.NeverReceived || universe.watchdogStatus == UniverseStatus.Expired)) { receiving.push(universe.universe); universe.watchdogStatus = UniverseStatus.Valid; } if (lastValidTime > universe.lastReceived && universe.status == UniverseStatus.Valid) { lost.push(universe.universe); universe.watchdogStatus = UniverseStatus.Expired; } } if (receiving.length > 0) { this.emit(this.onStatus, new SACNReceiving(receiving)); } if (lost.length > 0) { this.emit(this.onStatus, new SACNLost(lost)); } } withOnStatusHandler(handler: (status: SACNStatus) => any) { this.onStatus(handler); return this; } withOnErrorHandler(handler: (error: Error) => any) { this.onError(handler); return this; } static getUniverses(config: Config): Array<number> { function findAndPush(sacn: any) { if(sacn && !_.find(universes, universe => universe === sacn.universe)) { universes.push(sacn.universe); } } let universes = new Array<number>(); for (const command of config.commands) { findAndPush(command.sacn); for (const parameter of command.parameters) { findAndPush(parameter.sacn); } } return universes; } protected emit<Args extends any[]>(event: EventBinder<Args>, ...args: Args) { try { super.emit(event, ...args); } catch (error) { sentry(error); } } onStatus = this.registerEvent<(status: SACNStatus) => any>(); onError = this.registerEvent<(error: SACNError) => any>(); } export class SACNStatus { readonly universes: Array<number>; constructor(universes: Array<number>) { this.universes = universes; } } export class SACNWaiting extends SACNStatus { } export class SACNReceiving extends SACNStatus { } export class SACNLost extends SACNStatus { } export class SACNStopped extends SACNStatus { constructor() { super(null); } } export class SACNError extends Error { constructor(message?: string) { super(message); Object.setPrototypeOf(this, SACNError.prototype); this.name = SACNError.name; } } export class SACNCorruptError extends SACNError { constructor() { super("sACN error: Unexpected packet received. Are you using the \"final\" " + "protocol version in your MA sACN settings?"); Object.setPrototypeOf(this, SACNCorruptError.prototype); this.name = SACNCorruptError.name; } }