diff --git a/core/asset.js b/core/asset.js index 566add7c..063c115b 100644 --- a/core/asset.js +++ b/core/asset.js @@ -13,7 +13,7 @@ exports.getModuleAsset = getModuleAsset; exports.resolveConfigAsset = resolveConfigAsset; exports.getViewPropertyAsset = getViewPropertyAsset; -var ALL_ASSETS = [ +const ALL_ASSETS = [ 'art', 'menu', 'method', diff --git a/core/bbs.js b/core/bbs.js index 552660f3..0d6453d1 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -169,6 +169,10 @@ function initialize(cb) { }, function readyMessageNetworkSupport(callback) { require('./msg_network.js').startup(callback); + }, + function readyEventScheduler(callback) { + const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; + EventSchedulerModule.loadAndStart(callback); } ], function onComplete(err) { diff --git a/core/config.js b/core/config.js index 2fccab82..a305c138 100644 --- a/core/config.js +++ b/core/config.js @@ -211,16 +211,25 @@ function getDefaultConfig() { archivers : { zip : { - sig : "504b0304", + sig : '504b0304', offset : 0, - compressCmd : "7z", - compressArgs : [ "a", "-tzip", "{archivePath}", "{fileList}" ], - decompressCmd : "7z", - decompressArgs : [ "e", "-o{extractPath}", "{archivePath}" ] + compressCmd : '7z', + compressArgs : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], + decompressCmd : '7z', + decompressArgs : [ 'e', '-o{extractPath}', '{archivePath}' ] } }, + + + messageAreaDefaults : { + // + // The following can be override per-area as well + // + maxMessages : 1024, // 0 = unlimited + maxAgeDays : 0, // 0 = unlimited + }, - messageConferences : { + messageConferences : { system_internal : { name : 'System Internal', desc : 'Built in conference for private messages, bulletins, etc.', @@ -256,6 +265,25 @@ function getDefaultConfig() { bundleTargetByteSize : 2048000, // 2M, before creating another archive } }, + + eventScheduler : { + + + events : { + trimMessageAreas : { + // may optionally use [or ]@watch:/path/to/file + schedule : 'every 24 hours after 3:30 am', + + // action: + // - @method:path/to/module.js:theMethodName + // (path is relative to engima base dir) + // + // - @execute:/path/to/something/executable.sh + // + action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', // see method for avail args + } + } + }, misc : { idleLogoutSeconds : 60 * 6, // 6m diff --git a/core/database.js b/core/database.js index 3baf682a..5029d7e1 100644 --- a/core/database.js +++ b/core/database.js @@ -169,17 +169,19 @@ function createMessageBaseTables() { 'END;' ); + // :TODO: need SQL to ensure cleaned up if delete from message? dbs.message.run( 'CREATE TABLE IF NOT EXISTS message_meta (' + ' message_id INTEGER NOT NULL,' + ' meta_category INTEGER NOT NULL,' + ' meta_name VARCHAR NOT NULL,' + ' meta_value VARCHAR NOT NULL,' + - ' UNIQUE(message_id, meta_category, meta_name, meta_value),' + // why unique here? + ' UNIQUE(message_id, meta_category, meta_name, meta_value),' + // :TODO:why unique here? ' FOREIGN KEY(message_id) REFERENCES message(message_id)' + ');' ); + // :TODO: need SQL to ensure cleaned up if delete from message? dbs.message.run( 'CREATE TABLE IF NOT EXISTS hash_tag (' + ' hash_tag_id INTEGER PRIMARY KEY,' + @@ -188,6 +190,7 @@ function createMessageBaseTables() { ');' ); + // :TODO: need SQL to ensure cleaned up if delete from message? dbs.message.run( 'CREATE TABLE IF NOT EXISTS message_hash_tag (' + ' hash_tag_id INTEGER NOT NULL,' + diff --git a/core/door.js b/core/door.js index c50503cd..2862be4b 100644 --- a/core/door.js +++ b/core/door.js @@ -1,24 +1,24 @@ /* jslint node: true */ 'use strict'; -var spawn = require('child_process').spawn; -var events = require('events'); +const events = require('events'); -var _ = require('lodash'); -var pty = require('ptyw.js'); -var decode = require('iconv-lite').decode; -var net = require('net'); -var async = require('async'); +const _ = require('lodash'); +const pty = require('ptyw.js'); +const decode = require('iconv-lite').decode; +const createServer = require('net').createServer; exports.Door = Door; function Door(client, exeInfo) { events.EventEmitter.call(this); - this.client = client; - this.exeInfo = exeInfo; - + const self = this; + this.client = client; + this.exeInfo = exeInfo; this.exeInfo.encoding = this.exeInfo.encoding || 'cp437'; + this.exeInfo.encoding = this.exeInfo.encoding.toLowerCase(); + let restored = false; // // Members of exeInfo: @@ -31,25 +31,17 @@ function Door(client, exeInfo) { // dropFile // node // inhSocket - // -} + // -require('util').inherits(Door, events.EventEmitter); - - - -Door.prototype.run = function() { - - var self = this; - - var doorData = function(data) { - // :TODO: skip decoding if we have a match, e.g. cp437 === cp437 - self.client.term.write(decode(data, self.exeInfo.encoding)); + this.doorDataHandler = function(data) { + if(self.client.term.outputEncoding === self.exeInfo.encoding) { + self.client.term.rawWrite(data); + } else { + self.client.term.write(decode(data, self.exeInfo.encoding)); + } }; - var restored = false; - - var restore = function(piped) { + this.restoreIo = function(piped) { if(!restored && self.client.term.output) { self.client.term.output.unpipe(piped); self.client.term.output.resume(); @@ -57,100 +49,98 @@ Door.prototype.run = function() { } }; - var sockServer; + this.prepareSocketIoServer = function(cb) { + if('socket' === self.exeInfo.io) { + const sockServer = createServer(conn => { - async.series( - [ - function prepareServer(callback) { - if('socket' === self.exeInfo.io) { - sockServer = net.createServer(function connected(conn) { + sockServer.getConnections( (err, count) => { - sockServer.getConnections(function connCount(err, count) { + // We expect only one connection from our DOOR/emulator/etc. + if(!err && count <= 1) { + self.client.term.output.pipe(conn); + + conn.on('data', self.doorDataHandler); - // We expect only one connection from our DOOR/emulator/etc. - if(!err && count <= 1) { - self.client.term.output.pipe(conn); - - conn.on('data', doorData); - - conn.on('end', function ended() { - restore(conn); - }); - - conn.on('error', function error(err) { - self.client.log.info('Door socket server connection error: ' + err.message); - restore(conn); - }); - } + conn.once('end', () => { + return self.restoreIo(conn); }); - }); - sockServer.listen(0, function listening() { - callback(null); - }); - } else { - callback(null); - } - }, - function launch(callback) { - // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS - var args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified - - for(var i = 0; i < args.length; ++i) { - args[i] = self.exeInfo.args[i].format({ - dropFile : self.exeInfo.dropFile, - node : self.exeInfo.node.toString(), - //inhSocket : self.exeInfo.inhSocket.toString(), - srvPort : sockServer ? sockServer.address().port.toString() : '-1', - userId : self.client.user.userId.toString(), - }); - } - - var door = pty.spawn(self.exeInfo.cmd, args, { - cols : self.client.term.termWidth, - rows : self.client.term.termHeight, - // :TODO: cwd - env : self.exeInfo.env, - }); - - if('stdio' === self.exeInfo.io) { - self.client.log.debug('Using stdio for door I/O'); - - self.client.term.output.pipe(door); - - door.on('data', doorData); - - door.on('close', function closed() { - restore(door); - }); - } else if('socket' === self.exeInfo.io) { - self.client.log.debug( - { port : sockServer.address().port }, - 'Using temporary socket server for door I/O'); - } - - door.on('exit', function exited(code) { - self.client.log.info( { code : code }, 'Door exited'); - - if(sockServer) { - sockServer.close(); + conn.once('error', err => { + self.client.log.info( { error : err.toString() }, 'Door socket server connection'); + return self.restoreIo(conn); + }); } - - // we may not get a close - if('stdio' === self.exeInfo.io) { - restore(door); - } - - door.removeAllListeners(); - - self.emit('finished'); }); - } - ], - function complete(err) { - if(err) { - self.client.log.warn( { error : err.toString() }, 'Failed executing door'); - } + }); + + sockServer.listen(0, () => { + return cb(null, sockServer); + }); + } else { + return cb(null); } - ); -}; \ No newline at end of file + }; +} + +require('util').inherits(Door, events.EventEmitter); + +Door.prototype.run = function() { + const self = this; + + this.prepareSocketIoServer( (err, sockServer) => { + if(err) { + this.client.log.warn( { error : err.toString() }, 'Failed executing door'); + return self.emit('finished'); + } + + // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS + let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified + + for(let i = 0; i < args.length; ++i) { + args[i] = self.exeInfo.args[i].format({ + dropFile : self.exeInfo.dropFile, + node : self.exeInfo.node.toString(), + srvPort : sockServer ? sockServer.address().port.toString() : '-1', + userId : self.client.user.userId.toString(), + }); + } + + const door = pty.spawn(self.exeInfo.cmd, args, { + cols : self.client.term.termWidth, + rows : self.client.term.termHeight, + // :TODO: cwd + env : self.exeInfo.env, + }); + + if('stdio' === self.exeInfo.io) { + self.client.log.debug('Using stdio for door I/O'); + + self.client.term.output.pipe(door); + + door.on('data', self.doorDataHandler); + + door.once('close', () => { + return self.restoreIo(door); + }); + } else if('socket' === self.exeInfo.io) { + self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O'); + } + + door.once('exit', exitCode => { + self.client.log.info( { exitCode : exitCode }, 'Door exited'); + + if(sockServer) { + sockServer.close(); + } + + // we may not get a close + if('stdio' === self.exeInfo.io) { + return self.restoreIo(door); + } + + door.removeAllListeners(); + + self.emit('finished'); + }); + }); +}; diff --git a/core/event_scheduler.js b/core/event_scheduler.js new file mode 100644 index 00000000..9ca0a5bd --- /dev/null +++ b/core/event_scheduler.js @@ -0,0 +1,202 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const PluginModule = require('./plugin_module.js').PluginModule; +const Config = require('./config.js').config; +const Log = require('./logger.js').log; + +const _ = require('lodash'); +const later = require('later'); +const path = require('path'); + +exports.getModule = EventSchedulerModule; +exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart + +exports.moduleInfo = { + name : 'Event Scheduler', + desc : 'Support for scheduling arbritary events', + author : 'NuSkooler', +}; + +const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:)([^\0]+)?$/; +const ACTION_REGEXP = /\@(method|execute)\:([^\0]+)?$/; + +class ScheduledEvent { + constructor(events, name) { + this.name = name; + this.schedule = this.parseScheduleString(events[name].schedule); + this.action = this.parseActionSpec(events[name].action); + if(this.action) { + this.action.args = events[name].args; + } + } + + get isValid() { + if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { + return false; + } + + if('method' === this.action.type && !this.action.location) { + return false; + } + + return true; + } + + parseScheduleString(schedStr) { + if(!schedStr) { + return false; + } + + let schedule = {}; + + const m = SCHEDULE_REGEXP.exec(schedStr); + if(m) { + schedStr = schedStr.substr(0, m.index).trim(); + + if('@watch:' === m[1]) { + schedule.watchFile = m[2]; + } + } + + if(schedStr.length > 0) { + const sched = later.parse.text(schedStr); + if(-1 === sched.error) { + schedule.sched = sched; + } + } + + // return undefined if we couldn't parse out anything useful + if(!_.isEmpty(schedule)) { + return schedule; + } + } + + parseActionSpec(actionSpec) { + if(actionSpec) { + if('@' === actionSpec[0]) { + const m = ACTION_REGEXP.exec(actionSpec); + if(m) { + if(m[2].indexOf(':') > -1) { + const parts = m[2].split(':'); + return { + type : m[1], + location : parts[0], + what : parts[1], + }; + } else { + return { + type : m[1], + what : m[2], + }; + } + } + } else { + return { + type : 'execute', + what : actionSpec, + }; + } + } + } +} + +function EventSchedulerModule(options) { + PluginModule.call(this, options); + + if(_.has(Config, 'eventScheduler')) { + this.moduleConfig = Config.eventScheduler; + } + + const self = this; + this.runningActions = new Set(); + + this.performAction = function(schedEvent) { + if(self.runningActions.has(schedEvent.name)) { + return; // already running + } + + self.runningActions.add(schedEvent.name); + + if('method' === schedEvent.action.type) { + const modulePath = path.join(__dirname, '../', schedEvent.action.location); // enigma-bbs base + supplied location (path/file.js') + try { + const methodModule = require(modulePath); + methodModule[schedEvent.action.what](schedEvent.action.args, err => { + if(err) { + Log.debug( + { error : err.toString(), eventName : schedEvent.name, action : schedEvent.action }, + 'Error while performing scheduled event action'); + } + + self.runningActions.delete(schedEvent.name); + }); + } catch(e) { + Log.warn( + { error : e.toString(), eventName : schedEvent.name, action : schedEvent.action }, + 'Failed to perform scheduled event action'); + + self.runningActions.delete(schedEvent.name); + } + } + }; +} + +// convienence static method for direct load + start +EventSchedulerModule.loadAndStart = function(cb) { + const loadModuleEx = require('./module_util.js').loadModuleEx; + + const loadOpts = { + name : path.basename(__filename, '.js'), + path : __dirname, + }; + + loadModuleEx(loadOpts, (err, mod) => { + if(err) { + return cb(err); + } + + const modInst = new mod.getModule(); + modInst.startup( err => { + return cb(err); + }); + }); +}; + +EventSchedulerModule.prototype.startup = function(cb) { + + this.eventTimers = []; + const self = this; + + if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { + const events = Object.keys(this.moduleConfig.events).map( name => { + return new ScheduledEvent(this.moduleConfig.events, name); + }); + + events.forEach( schedEvent => { + if(!schedEvent.isValid) { + Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); + return; + } + + if(schedEvent.schedule.sched) { + this.eventTimers.push(later.setInterval( () => { + self.performAction(schedEvent); + }, schedEvent.schedule.sched)); + } + + // :TODO: handle watchfile -> performAction + }); + } + + cb(null); +}; + +EventSchedulerModule.prototype.shutdown = function(cb) { + if(this.eventTimers) { + this.eventTimers.forEach( et => et.clear() ); + } + + cb(null); +}; diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index c3fa1233..308130c4 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -492,7 +492,7 @@ function Packet(options) { } else if(line.startsWith('--- ')) { // Tear Lines are tracked allowing for specialized display/etc. messageBodyData.tearLine = line; - } else if(/[ ]{1,2}(\* )?Origin\: /.test(line)) { // To spec is " * Origin: ..." + } else if(/[ ]{1,2}(\* )?Origin\: /.test(line)) { // To spec is " * Origin: ..." messageBodyData.originLine = line; endOfMessage = false; // Anything past origin is not part of the message body } else if(line.startsWith('SEEN-BY:')) { diff --git a/core/ftn_util.js b/core/ftn_util.js index f1d0860e..11fb9a15 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -225,7 +225,7 @@ function getOrigin(address) { Config.general.boardName; const addrStr = new Address(address).toString('5D'); - return ` * Origin: ${origin} (${addrStr})`; + return ` * Origin: ${origin} (${addrStr})`; } function getTearLine() { diff --git a/core/menu_module.js b/core/menu_module.js index 503829c1..e454ff58 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -132,6 +132,7 @@ function MenuModule(options) { if(err) { console.log(err) // :TODO: what to do exactly????? + return self.prevMenu(); } self.finishedLoading(); diff --git a/core/message.js b/core/message.js index 9323b790..9bffe415 100644 --- a/core/message.js +++ b/core/message.js @@ -243,7 +243,14 @@ Message.prototype.load = function(options, cb) { 'WHERE message_uuid=? ' + 'LIMIT 1;', [ options.uuid ], - function row(err, msgRow) { + (err, msgRow) => { + if(err) { + return callback(err); + } + if(!msgRow) { + return callback(new Error('Message (no longer) available')); + } + self.messageId = msgRow.message_id; self.areaTag = msgRow.area_tag; self.messageUuid = msgRow.message_uuid; diff --git a/core/message_area.js b/core/message_area.js index 45cd7301..323061dc 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -1,16 +1,17 @@ /* jslint node: true */ 'use strict'; -let msgDb = require('./database.js').dbs.message; -let Config = require('./config.js').config; -let Message = require('./message.js'); -let Log = require('./logger.js').log; -let checkAcs = require('./acs_util.js').checkAcs; -let msgNetRecord = require('./msg_network.js').recordMessage; +const msgDb = require('./database.js').dbs.message; +const Config = require('./config.js').config; +const Message = require('./message.js'); +const Log = require('./logger.js').log; +const checkAcs = require('./acs_util.js').checkAcs; +const msgNetRecord = require('./msg_network.js').recordMessage; -let async = require('async'); -let _ = require('lodash'); -let assert = require('assert'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); +const moment = require('moment'); exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; @@ -27,6 +28,7 @@ exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; exports.getMessageAreaLastReadId = getMessageAreaLastReadId; exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; exports.persistMessage = persistMessage; +exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]'; const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]'; @@ -120,50 +122,50 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) { // // Note that built in 'system_internal' is always ommited here // - let defaultConf = _.findKey(Config.messageConferences, o => o.default); - if(defaultConf) { - const acs = Config.messageConferences[defaultConf].acs || CONF_AREA_RW_ACS_DEFAULT; - if(true === disableAcsCheck || checkAcs(client, acs)) { - return defaultConf; - } - } + let defaultConf = _.findKey(Config.messageConferences, o => o.default); + if(defaultConf) { + const acs = Config.messageConferences[defaultConf].acs || CONF_AREA_RW_ACS_DEFAULT; + if(true === disableAcsCheck || checkAcs(client, acs)) { + return defaultConf; + } + } + + // just use anything we can + defaultConf = _.findKey(Config.messageConferences, (o, k) => { + const acs = o.acs || CONF_AREA_RW_ACS_DEFAULT; + return 'system_internal' !== k && (true === disableAcsCheck || checkAcs(client, acs)); + }); - // just use anything we can - defaultConf = _.findKey(Config.messageConferences, (o, k) => { - const acs = o.acs || CONF_AREA_RW_ACS_DEFAULT; - return 'system_internal' !== k && (true === disableAcsCheck || checkAcs(client, acs)); - }); - - return defaultConf; + return defaultConf; } function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { - // - // Similar to finding the default conference: - // Find the first entry marked 'default', if any. If found, check | client| against - // *read* ACS. If this fails, just find the first one we can that passes checks. - // - // It's possible that we end up with nothing! - // - confTag = confTag || getDefaultMessageConferenceTag(client); - - if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) { - const areaPool = Config.messageConferences[confTag].areas; - let defaultArea = _.findKey(areaPool, o => o.default); - if(defaultArea) { - const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read; - if(true === disableAcsCheck || checkAcs(client, readAcs)) { - return defaultArea; - } - } - - defaultArea = _.findKey(areaPool, (o, k) => { - const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read; - return (true === disableAcsCheck || checkAcs(client, readAcs)); - }); - - return defaultArea; - } + // + // Similar to finding the default conference: + // Find the first entry marked 'default', if any. If found, check | client| against + // *read* ACS. If this fails, just find the first one we can that passes checks. + // + // It's possible that we end up with nothing! + // + confTag = confTag || getDefaultMessageConferenceTag(client); + + if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) { + const areaPool = Config.messageConferences[confTag].areas; + let defaultArea = _.findKey(areaPool, o => o.default); + if(defaultArea) { + const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read; + if(true === disableAcsCheck || checkAcs(client, readAcs)) { + return defaultArea; + } + } + + defaultArea = _.findKey(areaPool, (o, k) => { + const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read; + return (true === disableAcsCheck || checkAcs(client, readAcs)); + }); + + return defaultArea; + } } function getMessageConferenceByTag(confTag) { @@ -171,26 +173,26 @@ function getMessageConferenceByTag(confTag) { } function getMessageAreaByTag(areaTag, optionalConfTag) { - const confs = Config.messageConferences; - - if(_.isString(optionalConfTag)) { - if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { - return confs[optionalConfTag].areas[areaTag]; - } - } else { - // - // No confTag to work with - we'll have to search through them all - // - var area; - _.forEach(confs, (v, k) => { - if(_.has(v, [ 'areas', areaTag ])) { - area = v.areas[areaTag]; - return false; // stop iteration - } - }); - - return area; - } + const confs = Config.messageConferences; + + if(_.isString(optionalConfTag)) { + if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { + return confs[optionalConfTag].areas[areaTag]; + } + } else { + // + // No confTag to work with - we'll have to search through them all + // + var area; + _.forEach(confs, (v) => { + if(_.has(v, [ 'areas', areaTag ])) { + area = v.areas[areaTag]; + return false; // stop iteration + } + }); + + return area; + } } function changeMessageConference(client, confTag, cb) { @@ -426,8 +428,8 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) { 'VALUES (?, ?, ?);', [ userId, areaTag, messageId ], function written(err) { - callback(err, true); // true=didUpdate - } + callback(err, true); // true=didUpdate + } ); } else { callback(null); @@ -440,11 +442,11 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) { { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, 'Failed updating area last read ID'); } else { - if(true === didUpdate) { - Log.trace( - { userId : userId, areaTag : areaTag, messageId : messageId }, - 'Area last read ID updated'); - } + if(true === didUpdate) { + Log.trace( + { userId : userId, areaTag : areaTag, messageId : messageId }, + 'Area last read ID updated'); + } } cb(err); } @@ -463,4 +465,127 @@ function persistMessage(message, cb) { ], cb ); +} + +// method exposed for event scheduler +function trimMessageAreasScheduledEvent(args, cb) { + + function trimMessageAreaByMaxMessages(areaInfo, cb) { + if(0 === areaInfo.maxMessages) { + return cb(null); + } + + msgDb.run( + `DELETE FROM message + WHERE message_id IN + (SELECT message_id + FROM message + WHERE area_tag = ? + ORDER BY message_id + LIMIT (MAX(0, (SELECT COUNT() + FROM message + WHERE area_tag = ?) - ${areaInfo.maxMessages} + )) + );`, + [ areaInfo.areaTag, areaInfo.areaTag], + err => { + if(err) { + Log.warn( { areaInfo : areaInfo, error : err.toString(), type : 'maxMessages' }, 'Error trimming message area'); + } else { + Log.debug( { areaInfo : areaInfo, type : 'maxMessages' }, 'Area trimmed successfully'); + } + return cb(err); + } + ); + } + + function trimMessageAreaByMaxAgeDays(areaInfo, cb) { + if(0 === areaInfo.maxAgeDays) { + return cb(null); + } + + msgDb.run( + `DELETE FROM message + WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`, + [ areaInfo.areaTag ], + err => { + if(err) { + Log.warn( { areaInfo : areaInfo, error : err.toString(), type : 'maxAgeDays' }, 'Error trimming message area'); + } else { + Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays' }, 'Area trimmed successfully'); + } + return cb(err); + } + ); + } + + async.waterfall( + [ + function getAreaTags(callback) { + let areaTags = []; + msgDb.each( + `SELECT DISTINCT area_tag + FROM message;`, + (err, row) => { + if(err) { + return callback(err); + } + areaTags.push(row.area_tag); + }, + err => { + return callback(err, areaTags); + } + ); + }, + function prepareAreaInfo(areaTags, callback) { + let areaInfos = []; + + // determine maxMessages & maxAgeDays per area + areaTags.forEach(areaTag => { + + let maxMessages = Config.messageAreaDefaults.maxMessages; + let maxAgeDays = Config.messageAreaDefaults.maxAgeDays; + + const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here + if(area) { + if(area.maxMessages) { + maxMessages = area.maxMessages; + } + if(area.maxAgeDays) { + maxAgeDays = area.maxAgeDays; + } + } + + areaInfos.push( { + areaTag : areaTag, + maxMessages : maxMessages, + maxAgeDays : maxAgeDays, + } ); + }); + + return callback(null, areaInfos); + }, + function trimAreas(areaInfos, callback) { + async.each( + areaInfos, + (areaInfo, next) => { + trimMessageAreaByMaxMessages(areaInfo, err => { + if(err) { + return next(err); + } + + trimMessageAreaByMaxAgeDays(areaInfo, err => { + return next(err); + }); + }); + }, + callback + ); + } + ], + err => { + return cb(err); + } + ); + } \ No newline at end of file diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 2a2ae0a6..030ce380 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1140,6 +1140,8 @@ function FTNMessageScanTossModule() { require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule); +// :TODO: *scheduled* portion of this stuff should probably use event_scheduler - @immediate would still use record(). + FTNMessageScanTossModule.prototype.startup = function(cb) { Log.info(`${exports.moduleInfo.name} Scanner/Tosser starting up`); diff --git a/mods/abracadabra.js b/mods/abracadabra.js index c617763c..665a5ffe 100644 --- a/mods/abracadabra.js +++ b/mods/abracadabra.js @@ -165,7 +165,7 @@ function AbracadabraModule(options) { const doorInstance = new door.Door(self.client, exeInfo); - doorInstance.on('finished', () => { + doorInstance.once('finished', () => { self.prevMenu(); }); diff --git a/mods/bbs_link.js b/mods/bbs_link.js index eb2f244a..92bab4e8 100644 --- a/mods/bbs_link.js +++ b/mods/bbs_link.js @@ -1,18 +1,16 @@ /* jslint node: true */ 'use strict'; -var MenuModule = require('../core/menu_module.js').MenuModule; -var Log = require('../core/logger.js').log; -var resetScreen = require('../core/ansi_term.js').resetScreen; +const MenuModule = require('../core/menu_module.js').MenuModule; +const resetScreen = require('../core/ansi_term.js').resetScreen; -var async = require('async'); -var _ = require('lodash'); -var http = require('http'); -var net = require('net'); -var crypto = require('crypto'); -var buffers = require('buffers'); +const async = require('async'); +const _ = require('lodash'); +const http = require('http'); +const net = require('net'); +const crypto = require('crypto'); -var packageJson = require('../package.json'); +const packageJson = require('../package.json'); /* Expected configuration block: diff --git a/mods/menu.hjson b/mods/menu.hjson index 78ec3f4a..5618f111 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -939,6 +939,10 @@ value: { command: "DL" } action: @menu:doorDarkLands } + { + value: { command: "DP" } + action: @menu:doorParty + } ] } @@ -1006,6 +1010,16 @@ door: tw } } + + doorParty: { + desc: Using DoorParty! + module: @systemModule:door_party + config: { + username: XXXXXXXX + password: XXXXXXXX + bbsTag: XX + } + } /////////////////////////////////////////////////////////////////////// // Message Area Menu /////////////////////////////////////////////////////////////////////// diff --git a/mods/msg_list.js b/mods/msg_list.js index fbffa162..c0b421a6 100644 --- a/mods/msg_list.js +++ b/mods/msg_list.js @@ -133,7 +133,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) { callback(0 === self.messageList.length ? new Error('No messages in area') : null); } else { messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) { - if(msgList && 0 === msgList.length) { + if(!msgList || 0 === msgList.length) { callback(new Error('No messages in area')); } else { self.messageList = msgList; @@ -210,6 +210,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) { function complete(err) { if(err) { self.client.log.error( { error : err.toString() }, 'Error loading message list'); + } cb(err); }