Files
comic.chat-heap/classes.js

1215 lines
36 KiB
JavaScript
Raw Permalink Normal View History

/* jslint node: true */
'use strict';
const fs = require('fs');
var net = require('net');
var tls = require('tls');
var WebSocketClient = require('websocket').client;
function log(msg) {
console.log(msg);
}
function debug(msg) {
console.log(msg);
}
class Arg {
// Arg.Make("field") == Arg.Make("field", "_field")
// == Arg.Make({name: "field", slot: "_field"})
// == Arg.Make(new Arg("field")) ;; etc
static Make(field, slot=null) {
if(field instanceof Arg) {
return field}
if(typeof field === 'string' || field instanceof String) {
return new Arg(field, slot)}
if(field.hasOwnProperty('name')) {
return new Arg(field, slot)}
throw new Error('[[Error:[source:'+Arg.Make
+']][type:[invalid]][invalid:['
+field+']]]');}
// "x" == Arg.Find(new Arg("field"), {field:"x"})
// == Arg.Find("foo.field", {foo:{field:"x"}})
// == Arg.Find(new Arg("field"), {field:"y"}, {field:"x"})
static Find(field, config={}, args=null) {
var obj = Arg.Make(field);
if(args && args[obj.Arg]) {
return args[obj.Arg];}
var arg = obj._name;
if(arg) {
var dotIX = arg.indexOf('.');
if(dotIX != -1) {
if(dotIX +1 < arg.length) {
var first = arg.substring(0, dotIX);
var rest = arg.substring(dotIX+1,arg.length);
if(config[first]) {
return Arg.Find(
new Arg({name:rest}),
config[first]);}}}
else if(config[arg]) {
return config[arg];}}}
// mutates object
static Set(object, field, value) {
object[Arg.Make(field).Slot] = value;}
static OPT(obj, path, fallback) {
if(!(obj && path)) return fallback;
var c = obj;
var parts = path.split('.');
for(p in parts) {
c = c[p];
if (!c) return fallback; }
return c[parts[parts.length-1]] || fallback; }
constructor(name, slot=null) {
if(!name) {
throw new Error('[[Error:[source:Arg.new]]'
+'[type:[missing]][missing:'
+'[name]]]')}
this._name = name.name ? name.name : name;
if(slot) this._slot = slot;
else if(name.slot) this._slot = name.slot;
if(name.arg) this._arg = name.arg;}
get Arg() {
return this._arg || this.Name }
get Name() {
return this._name.indexOf('.') === -1
? this._name
: this._name.substring(
this._name.lastIndexOf('.')+1,
this._name.length);}
get Slot() { return this._slot || '_' + this.Name }}
class File {
static IsFile(obj) {
return obj && obj instanceof File; }
static Make(file, slot=null, path=null) {
if(File.IsFile(file)) {
return file; }
else if(typeof file === 'string' || file instanceof String) {
return new File(file, slot, path); }
else if(file.hasOwnProperty('name')) {
return new File(file, slot, path); }
throw new Error('[[Error:[source:File.Make'
+']][type:[invalid]][invalid:['
+file+']]]')}
constructor(name, slot=null, path=null) {
if(name.arg) this._arg = name.arg;
this._name = name.name || name.arg || name;
this._slot = name.slot || slot;
if(name.path || path) {
if(name.path) {
path = name.path; }
if(path) {
if(Array.isArray(path)) {
path = path.join('/')}
path = path.split('/').filter(
x => x && x.length > 0
&& x.indexOf('.') === -1);
if(path && path.length > 0) {
this._path = path;}}}}
get Arg() { return this._arg || this.Name; }
get Name() { return this._name; }
get Slot() { return this._slot; }
get Path() { // list of parts (or false when no path)
return this._path && this._path.length
? [...this._path]
: false; }
get Full() { return (this.Path||[]).concat([this.Name])}}
class ComicChat {
static get BasePath() { return process.env.COMIC_CHAT_PATH || '.' }
static get ConfigName() { return 'comicchat' }
static get ConfigExt() { return 'json' }
static get DefaultProgramStrings() {
return {
version: 'UNKNOWN', program: 'none',
connect: true, ssl: true,
cchat : { host: 'localhost', port: '8021', sock: 'wss' }}}
static get Fields() { return [
{name:'program'}, {name:'version'},
{name: 'config', slot:'_configFile'},
'connect', 'ssl', 'cchat.host', 'cchat.port',
{name:'cchat.sock', arg: 'websocket'}]; }
static get ProgramArgs() { return require('minimist')(process.argv.slice(2)); }
static IsComicChatObject(obj) {
return obj && obj instanceof ComicChat; }
static IsSafeString(s) {
return (typeof s === 'string' || s instanceof String)
&& s.length > 0
&& !/[^a-zA-Z0-9_-]/.test(s);}
static FilePath(name) {
if(Array.isArray(name)) {
if(name.every(ComicChat.IsSafeString)) {
name = name.join('/');}
else {
throw new Error('[[Error:[source:'+ComicChat.FilePath
+']][type:[invalid]][invalid:['
+name.join('/')+']]]')}}
else if(! ComicChat.IsSafeString(name)) {
throw new Error('[[Error:[source:'+ComicChat.FilePath
+']][type:[invalid]][invalid:['
+name+']]]');}
return ComicChat.BasePath
+ '/' + name
+ '.' + ComicChat.ConfigExt;}
static ParseConstructorArgs(defaults={}, options=false) {
return options === false
? [ComicChat.DefaultProgramStrings, defaults]
: [{...ComicChat.DefaultProgramStrings, ...defaults}, options];}
static ReadFile(name, throwOnError=true){
var rv = {};
try {
rv = JSON.parse(
fs.readFileSync(
ComicChat.FilePath(
File.Make(name).Full)))}
catch(e) {
log('caught error reading '
+ name + ': ' + e
+ (throwOnError?' (rethrowing)':''));
if(throwOnError) {
throw e;}}
return rv;}
static SelectConfig(args, defaultFile=false) {
var configFile = [];
if(ComicChat.IsSafeString(args[1])) {
configFile = [args[1]];
args[1] = {config: [args[1]]};}
else if(Array.isArray(args[1])) {
configFile = args[1];
args[1] = {config: configFile};}
else if(args[1].config === false) {
configFile = [];}
else if(ComicChat.IsSafeString(args[1].config)) {
configFile = [args[1].config];}
else if(Array.isArray(args[1].config)
&& [...args[1].config
].every(ComicChat.IsSafeString)) {
configFile = args[1].config;}
else if(defaultFile !== false) {
configFile = [ defaultFile === true
? ComicChat.ConfigName
: defaultFile];}
return configFile;}
static Version(obj) {
var rv =
obj instanceof ComicChat
? { program: obj._program, version: obj._version }
: { program: ComicChat.DefaultProgramStrings.program,
version: ComicChat.DefaultProgramStrings.version };
return `${rv.program} at version: ${rv.version}`;}
static Connect(host, port, handler) {
const socket = new net.Socket();
socket.on('connect', handler);
socket.connect(port, host);
return socket;}
static ConnectSSL(host, port, handler) {
const socket = tls.connect(port, host);
socket.on('secureConnect', handler);
return socket; }
constructor(defaults={},options=false) {
this._ready = false;
this._version = ComicChat.DefaultProgramStrings.version;
this._program = ComicChat.DefaultProgramStrings.program;
var input = ComicChat.ParseConstructorArgs(defaults, options);
ComicChat.SelectConfig(input).map(function(f) {
var o = File.Make(f);
if(o.Slot) {
input[1][f.slot] = ComicChat.ReadFile(o);}
else {
input[1] = { ...input[1], ...ComicChat.ReadFile(o)};}
}.bind(this));
if(!this.dispatcher) {
this.dispatcher = new DataHandler(); }
this.config = {...input[0], ...input[1]};
this.init();
this.test();
if(this.Ready && this._connect) {
this.Connect(); }}
init() {
this.Fields.map(function(f) {
var a = new Arg(f);
var v = Arg.Find(a, this.config, ComicChat.ProgramArgs);
if(v) { Arg.Set( this, a, v); }
}.bind(this));}
test() {
if(! ComicChat.IsSafeString(this.Program)) {
log('unsafe program name: ' + this.Program);}
else if(this.Program == ComicChat.DefaultProgramStrings.program) {
log('a program has no name');}
else {
return this._ready = true;}
return this._ready = false;}
get Fields() { return ComicChat.Fields; }
get Program() { return this._program; }
get Ready() { return this._ready === true; }
get Version() { return ComicChat.Version(this); }
get Host() { return this._host; }
get Port() { return this._port; }
get WebsocketProtocol() { return this._sock; }
createSocket(host=this.Host,
port=this.Port,
handler=this.HandleConnect) {
var socket;
var cb = function() {
socket.on('error', log);
socket.on('data', this.HandleData.bind(this));
}.bind(this);
if (this._ssl === true) {
if(this._selfsigned === true) {
// enable self-signed certificates
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; }
socket = ComicChat.ConnectSSL(host, port, handler); }
else {
socket = ComicChat.Connect(host, port, handler); }
socket.setEncoding(this._encoding);
if(this.nodelay === true) {
socket.setNoDelay();}
return socket; }
Connect(throwOnError=true) {
try { socket = this.createSocket(); }
catch(e) {
if(throwOnError === true) {
throw new Error('TODO connect error' + e); }}
this.socket = socket;
return this.socket; }
on(data, callback, once=false) {
this.dispatcher.on(data, callback, once); }
on_once(data, callback) { this.on(data, callback, true)}
raw(data) {
data && this.socket.write(
data + '\n', this._encoding); }
HandleData(message) { this.dispatcher.dispatch(message); }
HandleConnect(args) {
log(args);
const listeners = this.listeners || [];
this.socket.on('data', this.HandleData.bind(this));}}
class ComicChatPublisher extends ComicChat {
static get Fields() {
return ComicChat.Fields.concat(
'nick', 'room', 'link',
{name:'irc.channels',
slot:'_rooms',
arg:'rooms'}); }
static get defaultOptions() {
return {
nick: 'petromeglyph',
room: '#dungeon',
link: 'http'}; }
constructor(defaults={},options=false) {
if(options === false) {
options = defaults;
defaults = ComicChatPublisher.defaultOptions; }
else {
defaults = {...ComicChatPublisher.defaultOptions,
...defaults}; }
super(defaults, options); }
get Fields() { return ComicChatPublisher.Fields }
get Nick() { return this._nick; }
set Nick(na) { this._nick = na; }
get Room() { return this._room; }
set Room(rm) { this._room = rm; }
MakeLink(rm=this.Room) {
return this._link + '://'
+ this.Host + ':' + this.Port
+ '/' + rm; }
get Link() { return this.MakeLink() }}
class Message {
constructor(data) { this._data = data; }
data() { return this._data; }
reduce() { return this._data ? this._data.toString() : ''; }
}
class DataParser {
static Is(obj) { return obj && obj instanceof DataParser; }
static Make(obj) {
return Is(obj, Class=DataParser) ? obj : new Class(obj); }
static MakeMessageParser(Class=Message) {
return function(d) { return new Class(d); }}
*[Symbol.iterator]() {
const vals = [ this._parser, this._handler, this._once ];
for(v in vals) {
++count;
yield v; }
return count; }
constructor(parser=null, handler=null, once=null) {
var options = parser && parser.hasOwnProperty('parser')
? {...parser }
: (parser && Array.isArray(parser)
? { parser: parser[0],
handler: parser[1],
once: parser[2]}
: { parser: parser,
handler: handler,
once: once});
if(options.parser && options.parser === true) {
options.parser = DataParser.MakeMessageParser(
options.parserClass || Message); }
if(options.parser) this._parser = options.parser;
if(options.handler) this._handler = options.handler;
if(options.once) this._once = true;}}
class DataHandler {
constructor(handlers=[], context=null) {
this._context = context;
this._handlers = [...handlers]}
dispatch(data, context=this._context) {
for (var i = 0; i < this._handlers.length; i++) {
const info = this._handlers[i][0](data);
if (info) {
this._handlers[i][1].apply(context, [info, data]);
if (this._handlers[i][2]) {
this._handlers.splice(i, 1); }}}}
on(parser, handler, once=false) {
this._handlers.push([parser, handler, once]); }
on_once(parser, handler) {
this.on(parser, handler, true); }}
class IRCMessage extends Message {
static Is(obj) {
return obj && obj instanceof IRCMessage; }
static Make(data) {
return IRCMessage.Is(obj) ? obj
: new IRCMessage(data); }
*[Symbol.iterator]() {
var count = 0;
for(data in this.data) {
++count;
yield data; }
return count; }
constructor(data) { super(this.parse(data)) }
parse(data=this.data) {
if(!Array.IsArray(data)){ data = [data]; }
return data.join("\n").split("\n").filter(
s => s
&& s.length > 0
&& (!s.indexOf("\n")))}}
class IRCDataHandler extends DataHandler {
on(parser, handler, once=false) {
super.on(data => parser.exec(data),
handler, once); }
dispatch(data) {
const msg = IRCMessage.Make(data);
for(var line in msg) {
super.disptch(line); }}}
class MessageListener {
static Is(obj) {
return obj && obj instanceof MessageListener; }
static Make(obj) {
return Is(obj) ? obj
: new MessageListener(obj); }
static Subscribe(thing, listeners, factory=MessageListener.Make) {
if(thing && thing.on) {
return listeners.map(
me => factory(me).on(thing))}}
static get NullHandler() {
return new Function(); }
// this._type = 'data';
// this._handler = MessageListener.NullHandler;
constructor(type, handler=MessageListener.NullHandler) {
const options = type && type.hasOwnProperty('type')
? { ...type }
: { type: type, handler: handler };
this._type = options.type || 'data'
if(options.handler) {
this._handler = options.handler; }}
get handler() { return this._handler; }
get type() { return this._type; }
on(thing) {
if(thing && thing.on
&& (typeof thing.on === 'function'
|| thing.on instanceof Function)) {
thing.on(this.type, this.handler); }}}
// class SocketListener {
// static Connect(host, port, handler) {
// const socket = new net.Socket();
// socket.on('connect', handler);
// socket.connect(port, host);
// return socket;}
// static ConnectSSL(host, port, handler) {
// const socket = tls.connect(port, host);
// socket.on('secureConnect', handler);
// return socket; }
// static get DefaultHandler() {
// return new Function(); }
// static get DefaultListeners() {
// return //['data','close','error'];
// [
// { type: 'data',
// handler: function(data) { this.HandleData(data) }},
// { type: 'connect',
// handler: function(socket) {
// this.Subscribe(socket,
// this.listeners.filter(
// x => x && x.type
// && x.type != 'connect')); }}]}
// get SSL() { return SocketListener.ConnectSSL; }
// get NonSSL { return SocketListener.Connect; }
// createSocket(host=this._host,
// port=this._port,
// handler=this.Subscribe) {
// if(this.socket) { //&& punt.howToCheckIfNeedNew) {
// return this.socket; }
// var socket;
// if (this._ssl === true) {
// if(this._selfsigned === true) {
// // enable self-signed certificates
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; }
// socket = this.SSL(host, port, handler); }
// else {
// socket = this.NonSSL(host, port, handler); }
// socket.setEncoding(this._encoding);
// if(this.nodelay === true) {
// socket.setNoDelay();}
// return socket; }
// connect(throwOnError=true) {
// // TODO: check for live connection?
// try {
// const socket = this.createSocket();
// this.socket = socket;}
// catch(e) {
// if(throwOnError === true) {
// throw new Error('TODO connect error' + e); }}
// return this.socket; }
// // this.dispatcher = new DataHandler();
// HandleData(message) { this.dispatcher.dispatch(message); }
// //this.listeners = SocketListener.DefaultListeners;
// Subscribe(socket, listeners=this.listeners) {
// MessageListener.Subscribe(socket, listeners); }
// constructor(options={}) {
// this.dispatcher = new DataHandler();
// this.listeners = SocketListener.DefaultListeners;
// //this.#_host;
// // this.#_nodelay = true;
// // this.#_encoding = 'utf-8';
// // this.#_port;
// // this.#_selfsigned = true;
// // this.#_ssl = true;
// //this.socket;
// if(!options) {
// options = {}; }
// else if(options.connect
// && (typeof options.connect === 'function'
// || options.connect instanceof Function)) {
// // single arg constructor call contains socket
// options = {socket:options}; }
// if(options.hasOwnProperty('ssl')) {
// this._ssl = options.ssl; }
// if(options.hasOwnProperty('selfsigned')) {
// this._selfsigned = options.selfsigned; }
// if(options.hasOwnProperty('nodelay')) {
// this._nodelay = options.nodelay; }
// if(options.hasOwnProperty('encoding')) {
// this._encoding = options.encoding; }
// if(options.socket) {
// this.socket = options.socket; }
// if(options.hasOwnProperty('connect')
// && !options.connect) {
// } //noop
// else { this.connect(); }}
// on(data, callback, once=false) {
// this.dispatcher.on(data, callback, once); }
// on_once(data, callback) { this.on(data, callback, true)}
// raw(data) {
// data && this.socket.write(
// data + '\n', this._encoding); }}
// START TESTING
var cc = new ComicChatPublisher({config: 'test', //['test',{slot:'test2',file:"test"}],
test:"OK", version:"1.0.1"});
// {config: 'test3', test:"OK", version:"1.0.1",program:"test3"}
cc.on(function() { return {}; }, function(...args) { log(args) })
console.log({program:cc.Program,ready:cc.Ready,obj:cc,link:cc.Link});
// START UNTESTED
// var wsConnection, wsRetryHandlerID = null;
// var wsopConnection, wsopRetryHandlerID = null;
const owners =
[ { nick: "corwin", flags: { op: true },
mask: "corwin!someone@fosshost/director/corwin"} ];
const commands = [ 'join', 'part', 'set', 'op', 'hup', 'version' ];
class ComicChatAuthenticatedClient {
static get maxSendHistory() { return 5 }
static get DefaultCerts() { return function(certPath) {
return {
ca: fs.readFileSync(`${certPath}/ca-crt.pem`),
key: fs.readFileSync(`${certPath}/client1-key.pem`),
cert: fs.readFileSync(`${certPath}/client1-crt.pem`),
requestCert: true,
rejectUnauthorized: true
}}(config.control.certs||'.') }
static get DefaultOptions() { return {
ca: "",
key: "",
cert: "",
host: 'server.localhost',
port: config.control.port || 8020,
rejectUnauthorized:true,
requestCert:true
}}
constructor(options) {
this.sendCount = 0;
this.sendQueue = [{serial:0,message:{type:'version'},
callback:function(){
log('AC<-version: ' + JSON.parse(m))}
}];
this.responseQueue = {};
this.config = { ...ComicChatAuthenticatedClient.DefaultOptions, ...options};
if(!this.config.ca) {
this.config = { ...this.config, ...ComicChatAuthenticatedClient.DefaultCerts};
}
this.socket = this.connect();
this.on('data', (data) => {
log('AC DATA: ' + data);
var replyJSON;
try {
replyJSON = JSON.parse( data);
if(! replyJSON && replyJSON.serial
&& Number.isInteger( replyJSON.serial)) {
throw new Error('reply serialization '
+ '(nothing to deque)')
}
const serial = replyJSON.serial;
const message = sendQueue[ serial];
if(! (message && serial == message.serial)) {
throw new Error('reply serialization (not found)')
}
if(! this.responseQueue[ serial ]) {
this.responseQueue[serial] = {sent:[message]};
} else {
this.responseQueue[serial].sent.push(message);
if(this.responseQueue[serial].sent.length >
ComicChatAuthenticatedClient.maxSendHistory) {
this.responseQueue[serial].sent.splice(
this.responseQueue[serial].sent.length -1, 1)
}
}
this.responseQueue[serial].reply = replyJSON;
// don't remove first entry
if( serial > 0) {
//this.sendQueue.splice( serial, 1);
this.sendQueue[ serial] = null;
}
if(message.callback) {
message.callback( replyJSON );
}
} catch (e) {
log('AC (error) Bad message ' + data);
log('AC (Error detail): ' + e);
return;
}
});
this.on('error', (error) => { console.log('AC error: ' + error)});
this.on('end', (data) => { console.log('AC Socket end event')});
}
connect(options=this.config) {
const socket = tls.connect(options, (c) => {
console.log('client connected',
socket.authorized ? 'authorized' : 'unauthorized');
console.log(c)
socket.write('{"type":"version"}');
process.stdin.pipe(socket);
process.stdin.resume();
});
socket.setEncoding('utf8');
socket.setNoDelay(true);
socket.setKeepAlive(true, 5000);
return socket;
}
on(sig,cb) {
this.socket.on(sig, cb);
}
send(message, callback) {
if(! this.socket) {
log('AS (send error) : no socket');
}
if(! messaage) {
// ZZ blank message handlong?
log('AS (send error) : no message');
}
if(! message.sendQueueSerial ) {
message.sendQueueSerial = ++this.sendCount;
sendQueue[ message.sendQueueSerial ] = {
message:message,
serial: message.sendQueueSerial,
callback:callback
};
//} else if (somehow.WeSend(SomethingElse).FirstInstead) {
//message = SomethingElse
} else {
// it's a retry
}
socket.write(JSON.stringify(sendQueue[ message.sendQueueSerial]));
}
}
// // make controler connection (New! Experimental!)
// var controller = function () {
// var host = config.control.host || 'localhost';
// var port = config.control.port || 8020;
// return new ComicChatAuthenticatedClient({
// host: host,
// port: port
// })}();
class ComicChatIRCRelay {
static get defaultOwners() {
return [
{ nick: "corwin", flags: { op: true },
mask: "corwin!someone@fosshost/director/corwin"
}];}
static get defaultCommands() {
return [
'join', 'part', 'set', 'op', 'hup', 'version'
];}
static get defautOptions() {
return {
cchat: {
nick: 'petroglyph',
room: '#dungeon',
host: 'localhost', // hidoi.moebros.org
port: 8081,
roomLink: 'http://localhost/#dungeon'
},
control: {
port: 8082,
host: 'edit.comic.chat',
certs: 'cert'
},
irc: {
nick: 'petromeglpyh',
user: 'relay',
real: 'comicchat',
channels: ['#comicchat'],
host: 'irc.libera.chat',
emitGreeting: false,
port: 6697,
ssl: true,
opers: owners,
oppre: null,
nicre: null,
cmds: commands,
cmdre: null,
},
};
}
constructor(options=defaultOptions) {
this._irc = null;
this._control = null;
this._irc = false;
this.config={...defaultOptions, options};
}
//log(text)
}
module.exports = { Arg: Arg, File: File };
/*
process.title = 'petroglyph';
function log (text) {
console.log("\n" + (new Date()) + "\n" + text);
}
const warn = (text) => log(text);
const err = (text) => log(text);
var net = require('net');
var tls = require('tls');
var WebSocketClient = require('websocket').client;
const fs = require('fs');
var args = require('minimist')(process.argv.slice(2));
const confPath = args.path || '.';
const confFile = args.conf || 'relay';
const defaultConfig = {
cchat: {
nick: 'petroglyph',
room: '#dungeon',
host: 'ws.comic.chat', // hidoi.moebros.org
port: 8021,
roomLink: 'http://ws.comic.chat/#dungeon'
},
control: {
port: 8022,
host: 'edit.comic.chat',
certs: '/git/comicchat-edit/cert'
},
irc: {
nick: 'petromeglpyh',
user: 'comic',
real: 'relay++beta',
channels: ['#dungeon',
'#comicchat', '#c2e2',
// '#fosshost', '#fosshost-social',
"##bigfoss", "##moshpit", "##apocalypse"
],
host: 'irc.libera.chat',
emitGreeting: false,
port: 6697,
ssl: true,
opers: owners,
oppre: null,
nicre: null,
cmds: commands,
cmdre: null,
},
};
//const dataPath = args.data || 'data/';
//const dataFiles = ["channel-ops"];
//const defaultData { "channel-ops": {} };
{
var _loadFile = function (path, ext, config, file) {
var v = {};
if(file.test(/[^a-z0-9_-]/)) {
//throw new Error("can't load conf (bad-filename " + file);
err("(Relay Startup) can't load conf (bad-filename) '" + file +"'");
} else {
const filePath = `${path}/${file}${ext}`;
try { v = JSON.parse(fs.readFileSync(filePath))}
catch (e) { v = {} }
}
return { ...config, ...v };
}
const loadConf = _loadFile.bind(null, confPath, ".json", defaultConfig, configFile);
// const loadFile = function(file) {
// if(dataFiles.indexOf(file) != -1) {
// const defaultConfig = defaultData[file] || {};
// return _loadFile(dataPath, ".json", defaultConfig, file);
// }
// warn("(relay) attempt to load unknown data-file '"+file+"'");
// return {};
// }
}
const config = loadConf();
// ty: https://medium.com/stackfame/how-to-run-shell-script-file-or-command-using-nodejs-b9f2455cb6b7
const util = require('util');
const exec = util.promisify(require('child_process').exec);
async function restartThisBot(irc, oper) {
try {
const { stdout, stderr } =
await exec('/git/petroglyph/start-relay+.sh');
//console.log('stdout:', stdout);
//console.log('stderr:', stderr);
}catch (err) {
console.error(`*** FAIL restarting (${oper}):`
+ JSON.stringify(err));
reply(irc, oper, `restarted failed: ${err}`);
};
};
// https://stackoverflow.com/a/3561711
function escapeRegex(s) {
return new String(s).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
const stripHostRE = new RegExp('^[@!]+');
// pass object or makes one from from nick+mask string
function nick2mask(nick) {
var o = Array.isArray(nick) ? nick : {
nick: nick, flags: {}, mask: ""
};
if(o && o.nick && o.nick != '') {
if(-1 == o.nick.indexOf( '@')) {
mask = '(?:'+str+'![^@]![^ ]+*?)';
} else if(matches = stripHostRE.exec(o.nick)) {
mask = o.nick;
o.nick = matches[0];
}
if(o && o.nick && o.nick !='' // object, contains nick
&& -1 === o.nick.indexOf('@') // without junk
&& -1 !== o.mask.indexOf('@')) // and mask with
return o;
}
return null;
}
function initOper(oper) {
var o = nick2mask(oper);
var oix = oper ? config.irc.oper.findIndex( o => o.nick == oper.nick ) : -1;
if(-1 !== oix) {
if(o)config.irc.opers[ oix ] = o;
else config.irc.opers.splice( oix, 1);
} else if(o) config.irc.opers.push( o);
return o;
}
function initOperConfig() {
//log( 'Config: ' + JSON.stringify( config));
config.irc.oppre = new RegExp('^:(?:'+(config.irc.opers.map(escapeRegex).join('|'))+')');
config.irc.pmpre = new RegExp('^:[^:]+PRIVMSG ' + escapeRegex(config.irc.nick) + ' :');
config.irc.cmdre = new RegExp('^('+(config.irc.cmds.join('|'))+')');
log("loaded command RE: " + config.irc.cmdre.toString());
}
function joinChannel(irc, channel, message) {
irc.raw('JOIN ' + channel);
if(true === config.cchat.emitGreeting)
reply(irc, channel, (message ? message
: 'Relaying to: '
+ config.cchat.roomLink
.replace(config.cchat.room, channel)));
}
function reply(irc, who, message) {
irc.raw('PRIVMSG ' + who + ' :' + message);
}
function parseOp(irc, who, op, value) {
var args = value.split(' ');
var command = args.splice(0,1)[0];
var thisOP = {irc:irc,who:who,op:op,value:value,
args:args,command:command};
if(! config.irc.cmds.includes( thisOP.command )) {
log("OP (error): unknown command '"
+ thisOP.command
+ " for " + thisOP.who
+ " (" + thisOP.value +")"
);
reply(irc, who, 'Huh? what is "' + command + '"?');
return;
}
switch(thisOP.command) {
case "hup":
restartThisBot(thisOP.irc,thisOP.who);
break;
case "part":
thisOP.channel = thisOP.args[0];
if(!thisOP.channel) {
log("OP (error): cannot part '" + thisOP.channel + " for "
+ thisOP.who + ": " + thisOP.value);
reply(thisOP.irc, thisOP.who,
'Hrmm, try "part #foo" (?!?: '
+ thisOP.channel + ')'
);
} else if(!config.irc.channels.includes( thisOP.channel)) {
log("OP (warning): part from unknown channel '"
+ thisOP.channel + " from " + thisOP.who);
reply(thisOP.irc, thisOP.who,
"I'm not on " + thisOP.channel
+ ", " + thisOP.who);
} else {
log("OP *part* " + thisOP.channel + " (" + thisOP.who + ")");
var ix = config.irc.channels.indexOf(thisOP.channel);
config.irc.channels.splice(ix, 1);
comicchatServerJoin(thisOP.channel, true);
irc.raw('PART ' + thisOP.channel);
}
break;
case "version":
var onControlReply = function(data) {
log('OP <- AP DATA: ' + data);
reply(thisOP.irc, thisOP.who, "response: " + data);
};
break;
case "join":
var channel = thisOP.args[0];
if(channel && channel.length && channel.indexOf('#') === 0) {
if(config.irc.channels.includes(channel)) {
log("OP (warning): duplicated join to '"
+ channel + " from " + thisOP.who);
reply(thisOP.irc, thisOP.who, "Already on " + channel);
} else {
log("OP *join* " + channel + " (" +who+ ")");
comicchatServerJoin(channel);
config.irc.channels.push(channel);
joinChannel(irc, channel); // blindly assuming this worked. yay.
reply(irc, who, "OK. I joined " + channel);
}
} else {
log("OP (error): cannot join '" + channel + " for "
+ who + ": " + value);
reply(irc, who, 'Hrmm, try "join #foo" (?!?: ' + channel + ')');
}
break;
default:
log("OP (warning): unknown command '" + command
+ "' from " + who + " (full: " +value+ ")");
reply(irc, who, "unkown command '" + commands[0] + "'");
}
}
initOperConfig();
// COMIC CHAT OPER FUNCTIONS
function isOper(op) {
// log('Op -> isOper? '
// + JSON.stringify({op:op, isArray:Array.isArray(op),
// test: config.irc.oppre.test( op[0]),
// includes: config.irc.opers.includes( op),
// }));
return Array.isArray( op ) // called with "info"?
? config.irc.oppre.test( op[0])
: config.irc.opers.includes( op);
}
function isPM(info) {
// log('Op -> isPM? '
// + JSON.stringify({info:info, botnick:config.irc.nick,
// pmpre:config.irc.pmpre.toString(),
// test:config.irc.pmpre.test( info[0])
// }));
return config.irc.pmpre.test( info[0]);
}
function setControl(irc, who, op, value) {
if (wsopConnection && wsopConnection.send) {
log('Op -> Set "'
+ op + '" => ' + JSON.stringify( value)
//+ JSON.stringify({who:who, op:op, value:value})
);
wsopConnection.send(JSON.stringify({
type: 'set',
control: op,
settings: value.split(' '),
author: who
}));
} else {
log('IRC->Op Problem with Op connection, not setting');
}
}
function sendMessage(info) {
if (wsConnection && wsConnection.send) {
//log('CC -> RELAY ' + info[1] + ': ' + info[2]);
log('CC -> RELAY (info) ' + JSON.stringify(info));
wsConnection.send(JSON.stringify({
type: 'message',
room: info[3] || config.cchat.room,
text: info[2],
author: info[1],
spoof: true
}));
} else {
log('IRC->CC Problem with CC connection, not relaying');
}
}
// COMIC CHAT CONNECTION
var comicchatServerJoin;
function makeComicChat () {
var reconnectFunction = function () {
if (wsRetryHandlerID === null) {
wsRetryHandlerID = setInterval(function () {
log('CC: Reconnecting...');
makeComicChat();
}, 10000);
}
};
function addHandlers (ws) {
ws.on('connect', function (connection) {
log('CC: Websocket client connected to comic chat.');
wsConnection = connection;
if (wsRetryHandlerID !== null) {
clearInterval(wsRetryHandlerID);
wsRetryHandlerID = null;
}
// Join room, announce to room that relay has joined.
comicchatServerJoin = function(channel, part) {
(part ?
[
{ type: 'message', room: channel, text: 'Fin.' },
{ type: 'part', room: channel }
] : [
{ type: 'join', room: channel },
{ type: 'message', room: channel, text: config.cchat.nick },
{ type: 'message', room: channel,
author: config.cchat.nick,
text: 'Hello everyone! ' + config.irc.nick + ' ' + channel +' messenger here.' }
]).forEach(function (message) {
connection.sendUTF(JSON.stringify(message));
});
}
config.irc.channels.forEach((channel) => comicchatServerJoin(channel));
connection.on('error', function (e) {
log('CC: Connection error', e);
reconnectFunction();
});
connection.on('close', function (e) {
log('CC: Connection closed', e);
reconnectFunction();
});
});
return ws;
}
var ws = addHandlers(new WebSocketClient());
ws.on('connectFailed', function (e) {
log('CC: Conenction failed', e);
reconnectFunction();
});
ws.connect('wss://' + config.cchat.host + ':' + config.cchat.port);
}
// COMIC CHAT CONTROLLER CONNECTION
makeComicChat();
// IRC CONNECTION
var irc = {};
irc.listeners = [];
irc.pingTimerID = null;
function makeIRC() {
var connectHandler = function () {
log('IRC: established connection, registering...');
irc.on(/^PING :(.+)$/i, function (info) {
irc.raw('PONG :' + info[1]);
});
irc.on(/^.+ 001 .+$/i, function () {
config.irc.channels.forEach(function(channel) {
joinChannel(irc, channel);
});
});
irc.on(/^:(.+)!.+@.+ PRIVMSG .+? :(.+)$/i, function(info) {
log('IRC -> PIRVMSG ' + JSON.stringify({oper:isOper(info)?true:false,
pm:isPM(info)?true:false,
info:info
}));
if(isPM(info)) {
if(isOper(info))
// setControl( irc, info[1], 'op', info[2])
parseOp( irc, info[1], 'op', info[2])
//else { // NOOP: PM from unknown
//}
} else {
var matches = /PRIVMSG (#[^ ]+) :/.exec(info[0]);
if(matches && matches[1]) {
info.push(matches[1]);
sendMessage(info);
} else {
log('WARN: no channel for message: '
+ JSON.stringify({info:info,matches:matches}));
}
}
});
irc.raw('NICK ' + config.irc.nick);
irc.raw('USER ' + config.irc.user + ' 8 * :' + config.irc.real);
if (irc.pingTimerID !== null) {
clearInterval(irc.pingTimerID);
}
irc.pingTimerID = setInterval(function () {
irc.raw('PING BONG');
}, 60000);
};
if (config.irc.ssl === true) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // Self-signed certificates
irc.socket = tls.connect(config.irc.port, config.irc.host);
irc.socket.on('secureConnect', connectHandler);
} else {
irc.socket = new net.Socket();
irc.socket.on('connect', connectHandler);
irc.socket.connect(config.irc.port, config.irc.host);
}
irc.socket.setEncoding('utf-8');
irc.socket.setNoDelay();
irc.handle = function (data) {
var info;
for (var i = 0; i < irc.listeners.length; i++) {
info = irc.listeners[i][0].exec(data);
if (info) {
irc.listeners[i][1](info, data);
if (irc.listeners[i][2]) {
irc.listeners.splice(i, 1);
}
}
}
};
irc.on = function (data, callback) {
irc.listeners.push([data, callback, false]);
};
irc.on_once = function (data, callback) {
irc.listeners.push([data, callback, true]);
};
irc.raw = function(data) {
if (data !== '') {
irc.socket.write(data + '\n', 'utf-8');
log('IRC -> ' + data);
}
};
irc.socket.on('data', function (data) {
data = data.split("\n");
for (var i = 0; i < data.length; i++) {
if (data[i] !== '') {
log('IRC <- ' + data[i]);
irc.handle(data[i].slice(0, -1));
}
}
});
irc.socket.on('close', function () {
makeIRC();
});
irc.socket.on('error', function () {
makeIRC();
});
}
makeIRC();
// */