/* 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(); // */