Skip to content

Commit

Permalink
feat: Add sACN status
Browse files Browse the repository at this point in the history
  • Loading branch information
schw4rzlicht committed Jun 14, 2020
1 parent 06e105a commit c14bdba
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 64 deletions.
10 changes: 5 additions & 5 deletions __tests__/PermissionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ test("Permission granted b/c user is owner", async () => {
expect(permissionCollector.godModeReasons).toContain("user is channel owner");
});

test.skip("sACN lock", async () => { // FIXME
test("sACN lock", async () => {

let sacnReceiver: any;
permissionController["permissionInstances"].forEach(permissionInstance => {
Expand Down Expand Up @@ -149,7 +149,7 @@ test.skip("sACN lock", async () => { // FIXME
expect.assertions(6);
});

test.skip("sACN lock status", async () => { // FIXME
test("sACN lock status", async () => {

config = loadConfig({
sacn: {
Expand All @@ -175,12 +175,12 @@ test.skip("sACN lock status", async () => { // FIXME

(sACNPermissionInstance["sACNReceiver"] as any).on.mock.calls[0][1](sendData);

await expect(statusHandler).toBeCalledWith(new SACNReceiving());
await expect(statusHandler).toBeCalledWith(new SACNLost());
await expect(statusHandler).toBeCalledWith(new SACNReceiving([1]));
await expect(statusHandler).toBeCalledWith(new SACNLost([1]));

(sACNPermissionInstance["sACNReceiver"] as any).on.mock.calls[0][1](sendData);

await expect(statusHandler).toBeCalledWith(new SACNReceiving());
await expect(statusHandler).toBeCalledWith(new SACNReceiving([1]));

sACNPermissionInstance.stop();

Expand Down
2 changes: 2 additions & 0 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test("Event handlers attached", () => {
twitch2Ma.onHelpExecuted = jest.fn();
twitch2Ma.onPermissionDenied = jest.fn();
twitch2Ma.onGodMode = jest.fn();
twitch2Ma.onNotice = jest.fn();
twitch2Ma.onError = jest.fn();

index.attachEventHandlers(twitch2Ma);
Expand All @@ -47,6 +48,7 @@ test("Event handlers attached", () => {
expect(twitch2Ma.onHelpExecuted).toBeCalled();
expect(twitch2Ma.onPermissionDenied).toBeCalled();
expect(twitch2Ma.onGodMode).toBeCalled();
expect(twitch2Ma.onNotice).toBeCalled();
expect(twitch2Ma.onError).toBeCalled();
});

Expand Down
36 changes: 34 additions & 2 deletions src/lib/Twitch2Ma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import {Config, Command, Parameter} from "./Config";
import {RuntimeInformation} from "./RuntimeInformation";

import {PermissionCollector, PermissionController, PermissionError} from "./PermissionController";

import SACNPermission, {
SACNLost,
SACNReceiving,
SACNStatus,
SACNStopped,
SACNWaiting
} from "./permissions/SACNPermission";

import CooldownPermission from "./permissions/CooldownPermission";
import OwnerPermission from "./permissions/OwnerPermission";
import ModeratorPermission from "./permissions/ModeratorPermission";
Expand All @@ -21,7 +30,6 @@ import type Telnet from "telnet-client";

import SourceMapSupport = require("source-map-support");
import _ = require("lodash");
import SACNPermission from "./permissions/SACNPermission";

const TelnetClient = require("telnet-client");

Expand Down Expand Up @@ -57,7 +65,8 @@ export default class Twitch2Ma extends EventEmitter {
this.telnet = new TelnetClient();

this.permissionController = new PermissionController()
.withPermissionInstance(new SACNPermission(config))
.withPermissionInstance(new SACNPermission(config)
.withOnStatusHandler(status => this.handleSACNStatus(status)))
.withPermissionInstance(new CooldownPermission())
.withPermissionInstance(new OwnerPermission())
.withPermissionInstance(new ModeratorPermission());
Expand All @@ -75,6 +84,7 @@ export default class Twitch2Ma extends EventEmitter {
.catch(() => {
throw new TelnetError("Could not connect to desk!")
})
.then(() => this.permissionController.start())
.then(() => this.telnetLogin())
.then(() => this.initTwitch());
}
Expand Down Expand Up @@ -226,6 +236,27 @@ export default class Twitch2Ma extends EventEmitter {
return _.isString(command.availableParameters) ? `Available parameters: ${command.availableParameters}` : "";
}

private handleSACNStatus(status: SACNStatus) {
switch(status.constructor) {
case SACNWaiting:
this.emit(this.onNotice, `sACN status: Waiting for universes: ${status.universes.join(", ")}`);
break;
case SACNReceiving:
this.emit(this.onNotice, `sACN status: Receiving universes: ${status.universes.join(", ")}`);
break;
case SACNLost:
this.emit(this.onNotice, `sACN status: Lost universes: ${status.universes.join(", ")}`);
break;
case SACNStopped:
this.emit(this.onNotice, "sACN status: Stopped listening.");
break;
default:
this.emit(this.onNotice, `sACN status: Received unknown status: ${typeof status}`);
// TODO sentry
break;
}
}

protected emit<Args extends any[]>(event: EventBinder<Args>, ...args: Args) {
try {
super.emit(event, ...args);
Expand All @@ -241,4 +272,5 @@ export default class Twitch2Ma extends EventEmitter {
onHelpExecuted = this.registerEvent<(channel: string, user: string, helpCommand?: string) => any>();
onPermissionDenied = this.registerEvent<(channel: string, user: string, command: string, reason: string) => any>();
onGodMode = this.registerEvent<(channel: string, user: string, reason: string) => any>();
onNotice = this.registerEvent<(message: string) => any>();
}
10 changes: 8 additions & 2 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ let twitch2Ma: Twitch2Ma;
export async function main() {

process.on("SIGINT", () => {
console.log(chalk`\n{bold Thank you for using twitch2ma} ❤️`);
exit(0);
});

Expand Down Expand Up @@ -79,6 +78,7 @@ export async function attachEventHandlers(twitch2Ma: Twitch2Ma): Promise<Twitch2
twitch2Ma.onPermissionDenied((channel, user, command, reason) => channelMessage(channel,
chalk`✋ User {bold ${user}} tried to run {bold.blue ${command}} but permissions were denied by ${reason}.`))

twitch2Ma.onNotice(notice);
twitch2Ma.onError(exitWithError);

return twitch2Ma;
Expand Down Expand Up @@ -117,7 +117,9 @@ async function exit(statusCode: number) {
process.exit(1);
})
}
return stopPromise.then(() => process.exit(statusCode));
return stopPromise
.then(() => console.log(chalk`\n{bold Thank you for using twitch2ma} ❤️`))
.then(() => process.exit(statusCode));
}

export async function exitWithError(err: Error) {
Expand All @@ -137,6 +139,10 @@ function warning(message: string): void {
console.warn(chalk`⚠️ {yellow ${message}}`)
}

function notice(message: string): void {
console.log(chalk`ℹ️ {blue ${message}}`);
}

function error(message: string): void {
console.error(chalk`❌ {bold.red ${message}}`);
}
145 changes: 90 additions & 55 deletions src/lib/permissions/SACNPermission.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,48 @@
import {PermissionCollector, PermissionInstance} from "../PermissionController";
import {RuntimeInformation} from "../RuntimeInformation";
import {Config} from "../Config";
import {Receiver, Packet} from "sacn";
import {Packet, Receiver} from "sacn";
import {EventEmitter} from "@d-fischer/typed-event-emitter";

import _ = require("lodash");
import {EventBinder} from "@d-fischer/typed-event-emitter/lib/EventEmitter";
import {SACNUniverse, UniverseStatus} from "./SACNUniverse";
import _ = require("lodash");

export default class SACNPermission extends EventEmitter implements PermissionInstance {

private readonly watchUniverses: Array<number>;
private readonly universeData: Map<number, Array<number>>;
private readonly lastData: Map<number, number>;
private readonly universes: Array<SACNUniverse>;
private readonly config: Config;
private sACNReceiver: Receiver;
private status: SACNStatus;
private watchdogTimeout: NodeJS.Timeout;

constructor(config: Config) {
super();

this.universeData = new Map();
this.lastData = new Map();
this.universes = [];
this.config = config;

for (const command of this.config.commands) {
if (command.sacn) {
this.universeData.set(command.sacn.universe, null);
this.lastData.set(command.sacn.universe, 0);
this.universes[command.sacn.universe] = new SACNUniverse(command.sacn.universe);
}
for (const parameter of command.parameters) {
if (parameter.sacn) {
this.universeData.set(parameter.sacn.universe, null);
this.lastData.set(parameter.sacn.universe, 0);
this.universes[parameter.sacn.universe] = new SACNUniverse(parameter.sacn.universe);
}
}
}

this.universeData.size > 0 ? this.watchUniverses = Array.from(this.universeData.keys()) : [];
}

check(permissionCollector: PermissionCollector,
runtimeInformation: RuntimeInformation,
additionalRuntimeInformation: Map<String, any>): void {

if (this.status instanceof SACNReceiving && runtimeInformation.instructions) {
if (runtimeInformation.instructions) {

let sacn = runtimeInformation.instructions.sacn;

if (sacn) {
let universeData = this.universeData.get(sacn.universe);
if (universeData && _.isInteger(universeData[sacn.channel - 1]) && universeData[sacn.channel - 1] < 255) {
let universe = this.universes[sacn.universe];
if (universe && universe.data[sacn.channel - 1] < 255) {
permissionCollector.denyPermission("sacn",
`@${runtimeInformation.userName}, ${runtimeInformation.config.sacn.lockMessage}`);
}
Expand All @@ -59,35 +52,46 @@ export default class SACNPermission extends EventEmitter implements PermissionIn

start(): void {

if (this.watchUniverses.length > 0) {
if (this.universes.length > 0) {

let receiverOptions = {
universes: this.watchUniverses,
reuseAddr: true
};

if (this.config.sacn && _.isString(this.config.sacn.interface)) {
_.set(receiverOptions, "iface", this.config.sacn.interface);
let universes = [];
for (const universe of this.universes) {
if (universe) {
universes.push(universe.universe);
}
}

// FIXME Throws when interface does not exist, see https://github.com/k-yle/sACN/issues/19
this.sACNReceiver = new Receiver(receiverOptions);
if (universes.length > 0) {

this.sACNReceiver.on("packet", (packet: Packet) => {
let receiverOptions = {
universes: universes,
reuseAddr: true
};

if(_.includes(this.watchUniverses, packet.universe)) {
let data = new Array(512).fill(0);
packet.slotsData.forEach((value, channel) => {
data[channel] = value;
});

this.universeData.set(packet.universe, data);
this.lastData.set(packet.universe, new Date().getTime());
if (this.config.sacn && _.isString(this.config.sacn.interface)) {
_.set(receiverOptions, "iface", this.config.sacn.interface);
}
});

setTimeout(this.watchdog, this.config.sacn.timeout);
this.setStatus(new SACNWaiting(this.watchUniverses));
// FIXME Throws when interface does not exist, see https://github.com/k-yle/sACN/issues/21
this.sACNReceiver = new Receiver(receiverOptions);

this.sACNReceiver.on("packet", (packet: Packet) => {

if (this.universes[packet.universe]) {
let data = new Array(512).fill(0);
packet.slotsData.forEach((value, channel) => {
data[channel] = value;
});

this.universes[packet.universe].data = data;
}
});

this.emit(this.onStatus, new SACNWaiting(universes));

this.watchdogTimeout = setInterval(() => this.watchdog(), this.config.sacn.timeout);
this.watchdogTimeout.unref();
}
}
}

Expand All @@ -99,21 +103,48 @@ export default class SACNPermission extends EventEmitter implements PermissionIn
// TODO sentry
}
}
this.setStatus(new SACNStopped());
_.attempt(() => clearInterval(this.watchdogTimeout));
this.emit(this.onStatus, new SACNStopped());
}

private watchdog() {

/************************
** TODO status changes **
************************/
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.status == UniverseStatus.NeverReceived || universe.status == UniverseStatus.Expired)) {

receiving.push(universe.universe);
universe.status = UniverseStatus.Valid;
}

if (lastValidTime > universe.lastReceived && universe.status == UniverseStatus.Valid) {
lost.push(universe.universe);
universe.status = UniverseStatus.Expired;
}
}

if (receiving.length > 0) {
this.emit(this.onStatus, new SACNReceiving(receiving));
}

setTimeout(this.watchdog, this.config.sacn.timeout);
if (lost.length > 0) {
this.emit(this.onStatus, new SACNLost(lost));
}
}

private setStatus(status: SACNStatus) {
this.status = status;
this.emit(this.onStatus, status);
withOnStatusHandler(handler: (status: SACNStatus) => any) {
this.onStatus(handler);
return this;
}

protected emit<Args extends any[]>(event: EventBinder<Args>, ...args: Args) {
Expand All @@ -127,10 +158,7 @@ export default class SACNPermission extends EventEmitter implements PermissionIn
onStatus = this.registerEvent<(status: SACNStatus) => any>();
}

export interface SACNStatus {
}

export class SACNWaiting implements SACNStatus {
export class SACNStatus {

readonly universes: Array<number>;

Expand All @@ -139,11 +167,18 @@ export class SACNWaiting implements SACNStatus {
}
}

export class SACNReceiving implements SACNStatus {
export class SACNWaiting extends SACNStatus {
}

export class SACNLost implements SACNStatus {
export class SACNReceiving extends SACNStatus {
}

export class SACNStopped implements SACNStatus {
export class SACNLost extends SACNStatus {
}

export class SACNStopped extends SACNStatus {

constructor() {
super(null);
}
}
Loading

0 comments on commit c14bdba

Please sign in to comment.