Skip to content

Commit

Permalink
Add dist dir
Browse files Browse the repository at this point in the history
  • Loading branch information
Floofies committed Dec 8, 2021
1 parent b20b4d6 commit 1c905ad
Show file tree
Hide file tree
Showing 8 changed files with 509 additions and 2 deletions.
81 changes: 81 additions & 0 deletions dist/bin/gbelt_cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env node
import FileMonad from "../lib/FileMonad";
import CommandProcessor from "../lib/CLP";
import { defaultConfig } from "../index";
const helpString = [
"Goose Belt CLI syntax:",
"\t$ gbelt <command> <...operands>",
"Commands:",
"List all devices:\n\tlist",
"Set polling interval:\n\tpoll <seconds>",
"Add a device:\n\tadd <nickname> <host>",
"Remove a device:\n\tremove <nickname>"
].join("\n");
const confPath = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'] + "/.flock.json";
function missingOp() {
process.exitCode = 1;
return "Missing input operand";
}
async function cliReactor() {
const configFile = await new FileMonad(confPath, JSON.stringify(defaultConfig));
const config = JSON.parse(await configFile.read());
const reactor = {
_default: () => {
return helpString;
},
help: () => {
return helpString;
},
list: (nickname) => {
return "HTTP polling interval: " + config.pollrate + " seconds.";
// TODO: console.table(config.devices);
},
poll: async (pollrate) => {
config.pollrate = Number(pollrate);
await configFile.write(config);
},
add: async (nickname, host) => {
config.devices[nickname] = host;
await configFile.write(config);
},
remove: async (nickname) => {
if (delete config.devices[nickname])
await configFile.write(config);
}
};
const args = {
list: ["nickname?"],
poll: ["pollrate"],
add: ["nickname", "host"],
remove: ["nickname"]
};
// Read/write to the configuration file via the command-line
const cli = new CommandProcessor(reactor, args);
const output = await cli.exec(process.argv[2], process.argv[3], process.argv[4]) ?? "\n";
process.stdout.write(output);
}
/*
async function clp(command, op1, op2) {
const configFile = await new FileMonad(confPath, JSON.stringify(defaultConfig));
const config = JSON.parse(await configFile.read());
if (!command) return helpString;
else if (command === "list") {
return "HTTP polling interval: " + config.pollrate + " seconds.";
// TODO: console.table(config.devices);
} else if (command === "poll") {
if (!op1) return missingOp();
config.pollrate = op1;
await configFile.write(config);
} else if (command === "add") {
if (!op1 || !op2) return missingOp();
config.devices[op1] = op2;
await configFile.write(confPath, config);
} else if (command === "remove") {
if (!op1) return missingOp();
if (delete config.devices[op1])
await configFile.write(confPath, config);
} else {
process.exitCode = 1;
return `Unrecognized command \"${command}\".`;
}
}*/
163 changes: 163 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/usr/bin/env node
// SMS Alert Agent for ITWatchDogs MicroGoose
// Pop quiz: What is the sound made by a goose?
// Answer: Various loud honks, barks, and cackles. Also some hisses.
// The MicroGoose happens to speak XML, which sounds quite similar.
import xmlParser from "xml-js";
import FileMonad from "./lib/FileMonad.js";
import SyncMonad from "./lib/SyncMonad.js";
const isProd = process.env.NODE_ENV === "production";
const isTTY = process.stdout.isTTY;
const confPath = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'] + "/.flock.json";
const smsURL = "https://textbelt.com/text";
// Looks like cleanup on Aisle Four.
class HTTPError extends Error {
constructor(message) {
if (message instanceof Error)
super(message.message);
else
super(message);
this.name = "HTTPError";
}
}
;
async function httpFetch(url, opts) {
try {
const res = await fetch(url, opts);
// Throw an error if the HTTP Response is not acceptable.
if (!res.ok)
throw new HTTPError(`${res.status} ${res.statusText}`);
return res;
}
catch (err) {
throw new HTTPError(err.message);
}
}
// Send a HTTP POST request to the TextBelt API.
function httpSms(message) {
const smsOpts = {
key: process.env.smsKey,
phone: process.env.smsNum,
message: message,
};
const httpOpts = {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(smsOpts)
};
return httpFetch(smsURL, httpOpts);
}
async function notify(message) {
if (!isProd && isTTY)
console.log(message);
try {
await httpSms(message);
}
catch (err) {
console.error(err);
}
}
function alarmToString(alarm) {
return [
alarm["alarm-num"],
alarm["device-id"],
alarm.field,
alarm.limtype,
alarm.limit,
alarm.delay,
alarm.repeat,
alarm.email,
alarm.actions
].join("");
}
// Poll one goose at a time
const parserOptions = { compact: true };
async function pollGoose(host) {
// It speaks to us... That's one smart goose.
const res = await httpFetch("http://" + host + "/data.xml");
// This goose speaks XML, so we'll convert it to a JS object tree.
// "No way. Not possible," they said. But it's true.
const gooseData = xmlParser.xml2js(await res.text(), parserOptions);
// It's climate data and alarm data!
const data = {
devices: gooseData.server.devices.device,
alarms: gooseData.server.alarms.alarm
};
if (!Array.isArray(data.alarms))
data.alarms = [data.alarms];
if (!Array.isArray(data.devices))
data.devices = [data.devices];
return data;
}
export const defaultConfig = {
pollrate: 300,
devices: {}
};
// Hello! Come, come and bring your goose, and read with me.
export async function agent() {
// There is a configuration file, .flock.json.
// It will be inside the current user's home/userprofile folder.
// We will safely synchronize configuration with filesystem via a JSON state monad.
const confFile = new FileMonad(confPath, JSON.stringify(defaultConfig));
const state = new SyncMonad(confFile);
await state.syncState();
// The active Set records all currently tripped alarms as unique strings!
// Important to remember: The user might change the alarms mid-flight!
const active = new Set();
// poller sends 1 HTTP request to each MicroGoose device,
// and it expects XML in the responses.
(async function poller() {
// Get the most recent version of the configuration file.
const config = JSON.parse(state.getState());
for (const gooseName in config.devices)
try {
const host = config.devices[gooseName];
const data = await pollGoose(host);
for (const node of data.alarms) {
// Let's break these out for each alarm.
const alarm = node._attributes;
// Network topology is not known. Be careful.
const device = data.devices.find(device => device.id === alarm["device-id"]);
if (!device)
continue;
// TODO: Innards of device could all be undefined:
// Here be dragons
// We serialize the alarm into a unique string.
const alarmStr = alarmToString(alarm);
// We correlate the alarm with the device's climate data.
const curField = device.field.find((field) => field._attributes.key === alarm.field)._attributes;
// What is the name of your goose?
const nickname = `MicroGoose ${gooseName}`;
// We now have a description of an alarm, a sensor, and untripped/tripped status.
const statusStr = `${alarm.limtype} ${curField.niceName}${curField.value}${alarm.limit}`;
const tripped = alarm.status === "Tripped";
if (active.has(alarmStr)) {
// The alarm is in the active Set.
// If the alarm is still in tripped state,
// then the user was already notified.
// Avoid double-notification.
// TODO: Maybe make this tweakable..
if (tripped)
continue;
// The alarm changed to untripped status.
// Remove it from active, and notify the user.
active.delete(alarmStr);
notify(`✅CLEAR✅${nickname} UNTRIPPED ${statusStr}`);
}
else if (tripped) {
// The alarm was not in tripped status before, but is now,
// add it to active, and notify the user.
active.add(alarmStr);
notify(`⚠️ALERT⚠️${nickname} TRIPPED ${statusStr}`);
}
}
}
catch (err) {
console.error(err);
}
// Convert user's pollrate/seconds to miliseconds.
// Set timer for next HTTP poller attempt.
setTimeout(poller, config.pollrate * 1000);
})();
}
;
60 changes: 60 additions & 0 deletions dist/lib/CLP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Guns don’t kill mutants, I kill mutants.
function missingOps(operands) {
return () => `Command is missing input operands: "${operands.join(`", "`)}"`;
}
function invalidCmd(command) {
return () => `Command not recognized: "${String(command)}"`;
}
const _noCmd = () => `No command was given.`;
function noCmd() {
return _noCmd;
}
class Dispatcher {
constructor(init, args) {
this.executors = {};
this.args = {};
this.add(init, args);
}
add(init, args) {
for (const cmdStr in init)
if (args && cmdStr in args)
this.addCommand(cmdStr, init[cmdStr], args[cmdStr]);
else
this.addCommand(cmdStr, init[cmdStr]);
}
addCommand(cmdStr, executor, args) {
this.executors[cmdStr] = executor;
if (Array.isArray(args) && args.length)
this.args[cmdStr] = args;
else
this.args[cmdStr] = [];
}
// Find a matching executor for a given command:
parse(command, operands) {
if (!command)
if ("_default" in this.executors)
return this.executors._default;
else
return noCmd();
if (!(command in this.executors))
return invalidCmd(command);
const expOps = this.args[command];
if (expOps.length) {
if (!operands.length)
return missingOps(expOps);
if (operands.length !== expOps.length) {
const missing = expOps.slice(operands.length - 1, this.args[command].length - 1);
const required = missing.filter(op => op[op.length - 1] !== "?");
if (required.length)
return missingOps(required);
}
}
return this.executors[command];
}
// Execute a command immediately or return "false" if no executor:
async exec(command, ...operands) {
const executor = this.parse(command, operands);
return await executor(...operands);
}
}
export default Dispatcher;
55 changes: 55 additions & 0 deletions dist/lib/FileMonad.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// If you're reading this, then who is tending to the soufflé!?
import { constants as fsNumbers, promises as fs, watch as fsWatch } from "fs";
const FR = fsNumbers.F_OK | fsNumbers.R_OK;
const read = async function readFile(path, encoding) {
return await fs.readFile(path, { encoding });
};
// Mm. Smells like someone cut the fromage.
const ensure = async function ensureFile(path, data, encoding) {
try {
return await fs.access(path, FR);
}
catch (err) {
if (err.code === "ENOENT") {
return await fs.writeFile(path, data, { encoding });
}
else
throw err;
}
};
const write = function writeFile(path, data, encoding) {
return fs.writeFile(path, data, { encoding });
};
const watchOptions = {
persistent: false
};
// Provide an easy hook into fs.watch
const watch = function watchFile(path, targetFile, callback) {
fsWatch(path, watchOptions, async (type) => {
if (type !== "change")
return;
callback(targetFile);
});
};
// File reader, writer, and synchronizer.
// Someone's gonna ruin the soufflé.
class FileMonad {
constructor(path, data, encoding = "utf8") {
this.path = path;
this.read = async () => {
await ensure(path, data, encoding);
return await read(path, encoding);
};
this.ensure = (dData = data) => ensure(path, dData, encoding);
this.write = data => write(path, data, encoding);
this.watch = async (callback) => {
await ensure(path, data, encoding);
watch(path, this, callback);
};
}
get [Symbol.toStringTag]() {
return this.path;
}
}
export default FileMonad;
export { FileMonad };
12 changes: 12 additions & 0 deletions dist/lib/StateMonad.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class StateMonad {
constructor(state) {
this.getState = () => state;
this.setState = (newState) => { state = newState; };
if (state instanceof StateMonad)
this.setState(state.getState());
else
this.setState(state);
}
}
export default StateMonad;
export { StateMonad };
13 changes: 13 additions & 0 deletions dist/lib/SyncMonad.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StateMonad } from "./StateMonad.js";
// State monad which synchronizes with a file monad
class SyncMonad extends StateMonad {
constructor(file, init) {
super(init instanceof StateMonad ? init.getState() : init);
this.syncState = async () => {
this.setState(await file.read());
};
// Synchronize with the monad
file.watch(this.syncState);
}
}
export default SyncMonad;
Loading

0 comments on commit 1c905ad

Please sign in to comment.