2015-08-18 21:27:14 +00:00
|
|
|
/* jslint node: true */
|
|
|
|
'use strict';
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// ENiGMA½
|
|
|
|
const msgDb = require('./database.js').dbs.message;
|
|
|
|
const Config = require('./config.js').get;
|
|
|
|
const Message = require('./message.js');
|
|
|
|
const Log = require('./logger.js').log;
|
|
|
|
const msgNetRecord = require('./msg_network.js').recordMessage;
|
|
|
|
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
|
2018-11-23 21:47:18 +00:00
|
|
|
const UserProps = require('./user_property.js');
|
2018-11-28 04:21:00 +00:00
|
|
|
const StatLog = require('./stat_log.js');
|
|
|
|
const SysProps = require('./system_property.js');
|
2018-06-23 03:26:46 +00:00
|
|
|
|
|
|
|
// deps
|
|
|
|
const async = require('async');
|
|
|
|
const _ = require('lodash');
|
|
|
|
const assert = require('assert');
|
2018-11-28 04:21:00 +00:00
|
|
|
const moment = require('moment');
|
2015-08-18 21:27:14 +00:00
|
|
|
|
2018-11-28 04:21:00 +00:00
|
|
|
exports.startup = startup;
|
|
|
|
exports.shutdown = shutdown;
|
2016-02-03 04:35:59 +00:00
|
|
|
exports.getAvailableMessageConferences = getAvailableMessageConferences;
|
2018-06-23 03:26:46 +00:00
|
|
|
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
|
2016-02-03 04:35:59 +00:00
|
|
|
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
|
|
|
|
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
|
|
|
|
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
|
|
|
|
exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
|
2019-09-12 03:21:33 +00:00
|
|
|
exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags;
|
2018-06-23 03:26:46 +00:00
|
|
|
exports.getMessageConferenceByTag = getMessageConferenceByTag;
|
|
|
|
exports.getMessageAreaByTag = getMessageAreaByTag;
|
2020-04-25 17:25:47 +00:00
|
|
|
exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag;
|
2018-06-23 03:26:46 +00:00
|
|
|
exports.changeMessageConference = changeMessageConference;
|
|
|
|
exports.changeMessageArea = changeMessageArea;
|
2019-09-12 03:21:33 +00:00
|
|
|
exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead;
|
2019-09-17 04:05:03 +00:00
|
|
|
exports.hasMessageConfAndAreaWrite = hasMessageConfAndAreaWrite;
|
2019-09-12 03:21:33 +00:00
|
|
|
exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS;
|
2019-09-12 04:03:24 +00:00
|
|
|
exports.filterMessageListByReadACS = filterMessageListByReadACS;
|
2018-06-23 03:26:46 +00:00
|
|
|
exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea;
|
|
|
|
exports.getMessageListForArea = getMessageListForArea;
|
|
|
|
exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser;
|
|
|
|
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
|
|
|
|
exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea;
|
|
|
|
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
|
|
|
|
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
|
|
|
|
exports.persistMessage = persistMessage;
|
|
|
|
exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent;
|
2015-08-18 21:27:14 +00:00
|
|
|
|
2018-11-28 04:21:00 +00:00
|
|
|
function startup(cb) {
|
|
|
|
// by default, private messages are NOT included
|
|
|
|
async.series(
|
|
|
|
[
|
|
|
|
(callback) => {
|
|
|
|
Message.findMessages( { resultType : 'count' }, (err, count) => {
|
|
|
|
if(count) {
|
|
|
|
StatLog.setNonPersistentSystemStat(SysProps.MessageTotalCount, count);
|
|
|
|
}
|
|
|
|
return callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
(callback) => {
|
|
|
|
Message.findMessages( { resultType : 'count', date : moment() }, (err, count) => {
|
|
|
|
if(count) {
|
|
|
|
StatLog.setNonPersistentSystemStat(SysProps.MessagesToday, count);
|
|
|
|
}
|
|
|
|
return callback(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
],
|
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function shutdown(cb) {
|
|
|
|
return cb(null);
|
|
|
|
}
|
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getAvailableMessageConferences(client, options) {
|
2018-06-22 05:15:04 +00:00
|
|
|
options = options || { includeSystemInternal : false };
|
2017-02-20 18:31:24 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
assert(client || true === options.noClient);
|
2018-01-15 19:22:11 +00:00
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// perform ACS check per conf & omit system_internal if desired
|
2018-06-22 05:15:04 +00:00
|
|
|
return _.omitBy(Config().messageConferences, (conf, confTag) => {
|
|
|
|
if(!options.includeSystemInternal && 'system_internal' === confTag) {
|
|
|
|
return true;
|
|
|
|
}
|
2016-04-13 04:38:32 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
return client && !client.acs.hasMessageConfRead(conf);
|
|
|
|
});
|
2016-02-03 04:35:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getSortedAvailMessageConferences(client, options) {
|
2018-06-22 05:15:04 +00:00
|
|
|
const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => {
|
|
|
|
return {
|
|
|
|
confTag : k,
|
2018-06-23 03:26:46 +00:00
|
|
|
conf : v,
|
2018-06-22 05:15:04 +00:00
|
|
|
};
|
|
|
|
});
|
2016-02-03 04:35:59 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
sortAreasOrConfs(confs, 'conf');
|
2018-01-15 19:22:11 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
return confs;
|
2016-02-03 04:35:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return an *object* of available areas within |confTag|
|
|
|
|
function getAvailableMessageAreasByConfTag(confTag, options) {
|
2018-06-22 05:15:04 +00:00
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
// :TODO: confTag === "" then find default
|
|
|
|
|
|
|
|
const config = Config();
|
|
|
|
if(_.has(config.messageConferences, [ confTag, 'areas' ])) {
|
|
|
|
const areas = config.messageConferences[confTag].areas;
|
|
|
|
|
|
|
|
if(!options.client || true === options.noAcsCheck) {
|
2018-06-23 03:26:46 +00:00
|
|
|
// everything - no ACS checks
|
2018-06-22 05:15:04 +00:00
|
|
|
return areas;
|
|
|
|
} else {
|
2018-06-23 03:26:46 +00:00
|
|
|
// perform ACS check per area
|
2018-06-22 05:15:04 +00:00
|
|
|
return _.omitBy(areas, area => {
|
|
|
|
return !options.client.acs.hasMessageAreaRead(area);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2016-02-03 04:35:59 +00:00
|
|
|
}
|
2015-10-18 03:39:54 +00:00
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getSortedAvailMessageAreasByConfTag(confTag, options) {
|
2018-06-22 05:15:04 +00:00
|
|
|
const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => {
|
|
|
|
return {
|
2018-06-23 03:26:46 +00:00
|
|
|
areaTag : k,
|
|
|
|
area : v,
|
2018-06-22 05:15:04 +00:00
|
|
|
};
|
|
|
|
});
|
2018-01-15 19:22:11 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
sortAreasOrConfs(areas, 'area');
|
2018-01-15 19:22:11 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
return areas;
|
2015-08-20 22:35:04 +00:00
|
|
|
}
|
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getDefaultMessageConferenceTag(client, disableAcsCheck) {
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// Find the first conference marked 'default'. If found,
|
|
|
|
// inspect |client| against *read* ACS using defaults if not
|
|
|
|
// specified.
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// If the above fails, just go down the list until we get one
|
|
|
|
// that passes.
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// It's possible that we end up with nothing here!
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// Note that built in 'system_internal' is always ommited here
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
|
|
|
const config = Config();
|
|
|
|
let defaultConf = _.findKey(config.messageConferences, o => o.default);
|
|
|
|
if(defaultConf) {
|
|
|
|
const conf = config.messageConferences[defaultConf];
|
|
|
|
if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) {
|
|
|
|
return defaultConf;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// just use anything we can
|
|
|
|
defaultConf = _.findKey(config.messageConferences, (conf, confTag) => {
|
|
|
|
return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf));
|
|
|
|
});
|
|
|
|
|
|
|
|
return defaultConf;
|
2015-09-26 06:20:17 +00:00
|
|
|
}
|
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
const config = Config();
|
|
|
|
if(confTag && _.has(config.messageConferences, [ confTag, 'areas' ])) {
|
|
|
|
const areaPool = config.messageConferences[confTag].areas;
|
|
|
|
let defaultArea = _.findKey(areaPool, o => o.default);
|
|
|
|
if(defaultArea) {
|
|
|
|
const area = areaPool[defaultArea];
|
|
|
|
if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) {
|
|
|
|
return defaultArea;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-12 03:21:33 +00:00
|
|
|
defaultArea = _.findKey(areaPool, (area, areaTag) => {
|
|
|
|
if(Message.isPrivateAreaTag(areaTag)) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-06-22 05:15:04 +00:00
|
|
|
return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area));
|
|
|
|
});
|
|
|
|
|
|
|
|
return defaultArea;
|
|
|
|
}
|
2016-02-03 04:35:59 +00:00
|
|
|
}
|
2015-08-21 04:51:00 +00:00
|
|
|
|
2019-09-12 03:21:33 +00:00
|
|
|
function getSuitableMessageConfAndAreaTags(client) {
|
|
|
|
//
|
|
|
|
// Attempts to get a pair of suitable conf/area tags:
|
|
|
|
// * Where the client/user has proper ACS to both
|
|
|
|
// * Try to use defaults where possible
|
|
|
|
// * If default conf/area is not an option, use any
|
|
|
|
// that pass ACS.
|
|
|
|
// * Returns a tuple [confTag, areaTag]; areaTag
|
|
|
|
// and possibly confTag may both be set to '' if
|
|
|
|
// if we fail to find something.
|
|
|
|
//
|
|
|
|
let confTag = getDefaultMessageConferenceTag(client);
|
|
|
|
if(!confTag) {
|
|
|
|
return ['', '']; // can't have an area without a conf
|
|
|
|
}
|
|
|
|
|
|
|
|
let areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
|
|
|
if(!areaTag) {
|
|
|
|
// OK, perhaps *any* area in *any* conf?
|
|
|
|
_.forEach(Config().messageConferences, (conf, ct) => {
|
|
|
|
if(!client.acs.hasMessageConfRead(conf)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_.forEach(conf.areas, (area, at) => {
|
|
|
|
if(!_.includes(Message.WellKnownAreaTags, at) && client.acs.hasMessageAreaRead(area)) {
|
|
|
|
confTag = ct;
|
|
|
|
areaTag = at;
|
|
|
|
return false; // stop inner iteration
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if(areaTag) {
|
|
|
|
return false; // stop iteration
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return [confTag, areaTag || ''];
|
|
|
|
}
|
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getMessageConferenceByTag(confTag) {
|
2019-09-12 03:21:33 +00:00
|
|
|
return Object.assign({ confTag }, Config().messageConferences[confTag]);
|
2016-02-03 04:35:59 +00:00
|
|
|
}
|
|
|
|
|
2016-07-24 17:47:34 +00:00
|
|
|
function getMessageConfTagByAreaTag(areaTag) {
|
2018-06-22 05:15:04 +00:00
|
|
|
const confs = Config().messageConferences;
|
|
|
|
return Object.keys(confs).find( (confTag) => {
|
|
|
|
return _.has(confs, [ confTag, 'areas', areaTag]);
|
|
|
|
});
|
2016-07-24 17:47:34 +00:00
|
|
|
}
|
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getMessageAreaByTag(areaTag, optionalConfTag) {
|
2018-06-22 05:15:04 +00:00
|
|
|
const confs = Config().messageConferences;
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// :TODO: this could be cached
|
2018-06-22 05:15:04 +00:00
|
|
|
if(_.isString(optionalConfTag)) {
|
2019-09-12 03:56:33 +00:00
|
|
|
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
|
2019-09-12 03:21:33 +00:00
|
|
|
return Object.assign(
|
|
|
|
{
|
|
|
|
areaTag,
|
|
|
|
confTag : optionalConfTag,
|
|
|
|
},
|
|
|
|
confs[optionalConfTag].areas[areaTag]
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
//
|
|
|
|
// No confTag to work with - we'll have to search through them all
|
|
|
|
//
|
|
|
|
let area;
|
2019-09-12 03:21:33 +00:00
|
|
|
_.forEach(confs, (conf, confTag) => {
|
2019-09-12 03:56:33 +00:00
|
|
|
if(_.has(conf, [ 'areas', areaTag ])) {
|
2019-09-12 03:21:33 +00:00
|
|
|
area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]);
|
2018-06-22 05:15:04 +00:00
|
|
|
return false; // stop iteration
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return area;
|
|
|
|
}
|
2016-02-03 04:35:59 +00:00
|
|
|
}
|
2015-08-21 04:51:00 +00:00
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function changeMessageConference(client, confTag, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function getConf(callback) {
|
|
|
|
const conf = getMessageConferenceByTag(confTag);
|
|
|
|
|
|
|
|
if(conf) {
|
|
|
|
callback(null, conf);
|
|
|
|
} else {
|
|
|
|
callback(new Error('Invalid message conference tag'));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function getDefaultAreaInConf(conf, callback) {
|
2018-06-23 03:26:46 +00:00
|
|
|
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
|
|
|
const area = getMessageAreaByTag(areaTag, confTag);
|
2018-06-22 05:15:04 +00:00
|
|
|
|
|
|
|
if(area) {
|
|
|
|
callback(null, conf, { areaTag : areaTag, area : area } );
|
|
|
|
} else {
|
|
|
|
callback(new Error('No available areas for this user in conference'));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function validateAccess(conf, areaInfo, callback) {
|
|
|
|
if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(areaInfo.area)) {
|
|
|
|
return callback(new Error('Access denied to message area and/or conference'));
|
|
|
|
} else {
|
|
|
|
return callback(null, conf, areaInfo);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function changeConferenceAndArea(conf, areaInfo, callback) {
|
|
|
|
const newProps = {
|
2018-11-23 21:47:18 +00:00
|
|
|
[ UserProps.MessageConfTag ] : confTag,
|
|
|
|
[ UserProps.MessageAreaTag ] : areaInfo.areaTag,
|
2018-06-22 05:15:04 +00:00
|
|
|
};
|
|
|
|
client.user.persistProperties(newProps, err => {
|
|
|
|
callback(err, conf, areaInfo);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
],
|
|
|
|
function complete(err, conf, areaInfo) {
|
|
|
|
if(!err) {
|
|
|
|
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed');
|
|
|
|
} else {
|
|
|
|
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference');
|
|
|
|
}
|
|
|
|
cb(err);
|
|
|
|
}
|
|
|
|
);
|
2015-08-21 04:51:00 +00:00
|
|
|
}
|
|
|
|
|
2016-07-24 17:47:34 +00:00
|
|
|
function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
2018-06-23 03:26:46 +00:00
|
|
|
options = options || {}; // :TODO: this is currently pointless... cb is required...
|
2018-06-22 05:15:04 +00:00
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function getArea(callback) {
|
|
|
|
const area = getMessageAreaByTag(areaTag);
|
|
|
|
return callback(area ? null : new Error('Invalid message areaTag'), area);
|
|
|
|
},
|
|
|
|
function validateAccess(area, callback) {
|
|
|
|
//
|
|
|
|
// Need at least *read* to access the area
|
|
|
|
//
|
|
|
|
if(!client.acs.hasMessageAreaRead(area)) {
|
|
|
|
return callback(new Error('Access denied to message area'));
|
|
|
|
} else {
|
|
|
|
return callback(null, area);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function changeArea(area, callback) {
|
|
|
|
if(true === options.persist) {
|
2018-11-23 21:47:18 +00:00
|
|
|
client.user.persistProperty(UserProps.MessageAreaTag, areaTag, function persisted(err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return callback(err, area);
|
|
|
|
});
|
|
|
|
} else {
|
2018-11-23 21:47:18 +00:00
|
|
|
client.user.properties[UserProps.MessageAreaTag] = areaTag;
|
2018-06-22 05:15:04 +00:00
|
|
|
return callback(null, area);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
function complete(err, area) {
|
|
|
|
if(!err) {
|
|
|
|
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
|
|
|
|
} else {
|
|
|
|
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
2015-08-20 22:35:04 +00:00
|
|
|
}
|
2015-08-27 05:25:49 +00:00
|
|
|
|
2016-07-24 17:47:34 +00:00
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// Temporairly -- e.g. non-persisted -- change to an area and it's
|
|
|
|
// associated underlying conference. ACS is checked for both.
|
2016-07-24 17:47:34 +00:00
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// This is useful for example when doing a new scan
|
2016-07-24 17:47:34 +00:00
|
|
|
//
|
|
|
|
function tempChangeMessageConfAndArea(client, areaTag) {
|
2018-06-23 03:26:46 +00:00
|
|
|
const area = getMessageAreaByTag(areaTag);
|
|
|
|
const confTag = getMessageConfTagByAreaTag(areaTag);
|
2016-07-24 17:47:34 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
if(!area || !confTag) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-07-24 17:47:34 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
const conf = getMessageConferenceByTag(confTag);
|
2016-07-24 17:47:34 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-01-15 19:22:11 +00:00
|
|
|
|
2018-11-24 00:41:16 +00:00
|
|
|
client.user.properties[UserProps.MessageConfTag] = confTag;
|
|
|
|
client.user.properties[UserProps.MessageAreaTag] = areaTag;
|
2016-07-24 17:47:34 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
return true;
|
2016-07-24 17:47:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function changeMessageArea(client, areaTag, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb);
|
2016-07-24 17:47:34 +00:00
|
|
|
}
|
|
|
|
|
2019-09-12 03:56:33 +00:00
|
|
|
function hasMessageConfAndAreaRead(client, areaOrTag) {
|
|
|
|
if(_.isString(areaOrTag)) {
|
|
|
|
areaOrTag = getMessageAreaByTag(areaOrTag) || {};
|
|
|
|
}
|
|
|
|
const conf = getMessageConferenceByTag(areaOrTag.confTag);
|
|
|
|
return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag);
|
2019-09-12 03:21:33 +00:00
|
|
|
}
|
|
|
|
|
2019-09-17 04:05:03 +00:00
|
|
|
function hasMessageConfAndAreaWrite(client, areaOrTag) {
|
|
|
|
if(_.isString(areaOrTag)) {
|
|
|
|
areaOrTag = getMessageAreaByTag(areaOrTag) || {};
|
|
|
|
}
|
|
|
|
const conf = getMessageConferenceByTag(areaOrTag.confTag);
|
|
|
|
return client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag);
|
|
|
|
}
|
|
|
|
|
2019-09-12 03:21:33 +00:00
|
|
|
function filterMessageAreaTagsByReadACS(client, areaTags) {
|
|
|
|
if(!Array.isArray(areaTags)) {
|
|
|
|
areaTags = [ areaTags ];
|
|
|
|
}
|
|
|
|
|
|
|
|
return areaTags.filter( areaTag => {
|
|
|
|
const area = getMessageAreaByTag(areaTag);
|
|
|
|
return hasMessageConfAndAreaRead(client, area);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-09-12 04:03:24 +00:00
|
|
|
function filterMessageListByReadACS(client, messageList) {
|
|
|
|
//
|
|
|
|
// Filter out messages belonging to conf/areas the user
|
|
|
|
// doesn't have access to.
|
|
|
|
//
|
|
|
|
|
|
|
|
// Keep a cache around for quick lookup.
|
|
|
|
const acsCache = new Map(); // areaTag:boolean
|
|
|
|
|
|
|
|
return messageList.filter(msg => {
|
|
|
|
let cached = acsCache.get(msg.areaTag);
|
|
|
|
if(false === cached) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if(true === cached) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
cached = hasMessageConfAndAreaRead(client, msg.areaTag);
|
|
|
|
acsCache.set(msg.areaTag, cached);
|
|
|
|
return cached;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-01-27 04:42:43 +00:00
|
|
|
function getNewMessageCountInAreaForUser(userId, areaTag, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => {
|
|
|
|
lastMessageId = lastMessageId || 0;
|
|
|
|
|
|
|
|
const filter = {
|
|
|
|
areaTag,
|
2018-06-23 03:26:46 +00:00
|
|
|
newerThanMessageId : lastMessageId,
|
|
|
|
resultType : 'count',
|
2018-06-22 05:15:04 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if(Message.isPrivateAreaTag(areaTag)) {
|
|
|
|
filter.privateTagUserId = userId;
|
|
|
|
}
|
|
|
|
|
|
|
|
Message.findMessages(filter, (err, count) => {
|
|
|
|
return cb(err, count);
|
|
|
|
});
|
|
|
|
});
|
2016-07-24 17:47:34 +00:00
|
|
|
}
|
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getNewMessagesInAreaForUser(userId, areaTag, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => {
|
|
|
|
lastMessageId = lastMessageId || 0;
|
|
|
|
|
|
|
|
const filter = {
|
|
|
|
areaTag,
|
2018-06-23 03:26:46 +00:00
|
|
|
resultType : 'messageList',
|
|
|
|
newerThanMessageId : lastMessageId,
|
|
|
|
sort : 'messageId',
|
|
|
|
order : 'ascending',
|
2018-06-22 05:15:04 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if(Message.isPrivateAreaTag(areaTag)) {
|
|
|
|
filter.privateTagUserId = userId;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Message.findMessages(filter, cb);
|
|
|
|
});
|
2018-01-27 04:42:43 +00:00
|
|
|
}
|
2015-08-27 05:25:49 +00:00
|
|
|
|
2018-11-22 00:55:31 +00:00
|
|
|
function getMessageListForArea(client, areaTag, filter, cb)
|
|
|
|
{
|
|
|
|
if(!cb && _.isFunction(filter)) {
|
|
|
|
cb = filter;
|
|
|
|
filter = {
|
|
|
|
areaTag,
|
|
|
|
resultType : 'messageList',
|
|
|
|
sort : 'messageId',
|
|
|
|
order : 'ascending'
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
Object.assign(filter, { areaTag } );
|
|
|
|
}
|
2018-06-22 05:15:04 +00:00
|
|
|
|
2019-09-10 03:44:03 +00:00
|
|
|
if(client) {
|
2019-09-12 03:56:33 +00:00
|
|
|
if(!hasMessageConfAndAreaRead(client, areaTag)) {
|
2019-09-10 03:44:03 +00:00
|
|
|
return cb(null, []);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
if(Message.isPrivateAreaTag(areaTag)) {
|
2019-09-10 03:44:03 +00:00
|
|
|
filter.privateTagUserId = client ? client.user.userId : 'INVALID_USER_ID';
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return Message.findMessages(filter, cb);
|
2015-08-27 05:25:49 +00:00
|
|
|
}
|
2015-09-06 21:58:58 +00:00
|
|
|
|
2018-01-12 04:17:59 +00:00
|
|
|
function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
Message.findMessages(
|
|
|
|
{
|
|
|
|
areaTag,
|
|
|
|
newerThanTimestamp,
|
2018-06-23 03:26:46 +00:00
|
|
|
sort : 'modTimestamp',
|
|
|
|
order : 'ascending',
|
|
|
|
limit : 1,
|
2018-06-22 05:15:04 +00:00
|
|
|
},
|
|
|
|
(err, id) => {
|
|
|
|
if(err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
return cb(null, id ? id[0] : null);
|
|
|
|
}
|
|
|
|
);
|
2018-01-12 04:17:59 +00:00
|
|
|
}
|
|
|
|
|
2016-02-03 04:35:59 +00:00
|
|
|
function getMessageAreaLastReadId(userId, areaTag, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
msgDb.get(
|
2018-06-23 03:26:46 +00:00
|
|
|
'SELECT message_id ' +
|
|
|
|
'FROM user_message_area_last_read ' +
|
|
|
|
'WHERE user_id = ? AND area_tag = ?;',
|
2018-06-22 05:15:04 +00:00
|
|
|
[ userId, areaTag.toLowerCase() ],
|
|
|
|
function complete(err, row) {
|
|
|
|
cb(err, row ? row.message_id : 0);
|
|
|
|
}
|
|
|
|
);
|
2015-10-22 21:44:44 +00:00
|
|
|
}
|
|
|
|
|
2018-01-12 04:17:59 +00:00
|
|
|
function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
if(!cb && _.isFunction(allowOlder)) {
|
|
|
|
cb = allowOlder;
|
|
|
|
allowOlder = false;
|
|
|
|
}
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// :TODO: likely a better way to do this...
|
2018-06-22 05:15:04 +00:00
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function getCurrent(callback) {
|
|
|
|
getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) {
|
|
|
|
lastId = lastId || 0;
|
2018-06-23 03:26:46 +00:00
|
|
|
callback(null, lastId); // ignore errors as we default to 0
|
2018-06-22 05:15:04 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
function update(lastId, callback) {
|
|
|
|
if(allowOlder || messageId > lastId) {
|
|
|
|
msgDb.run(
|
2018-06-23 03:26:46 +00:00
|
|
|
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
|
|
|
|
'VALUES (?, ?, ?);',
|
2018-06-22 05:15:04 +00:00
|
|
|
[ userId, areaTag, messageId ],
|
|
|
|
function written(err) {
|
|
|
|
callback(err, true); // true=didUpdate
|
|
|
|
}
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
callback(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
function complete(err, didUpdate) {
|
|
|
|
if(err) {
|
|
|
|
Log.debug(
|
|
|
|
{ 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');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cb(err);
|
|
|
|
}
|
|
|
|
);
|
2015-10-22 21:44:44 +00:00
|
|
|
}
|
2016-02-17 05:11:55 +00:00
|
|
|
|
|
|
|
function persistMessage(message, cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
async.series(
|
|
|
|
[
|
|
|
|
function persistMessageToDisc(callback) {
|
|
|
|
return message.persist(callback);
|
|
|
|
},
|
|
|
|
function recordToMessageNetworks(callback) {
|
|
|
|
return msgNetRecord(message, callback);
|
|
|
|
}
|
|
|
|
],
|
|
|
|
cb
|
|
|
|
);
|
2016-06-20 03:09:45 +00:00
|
|
|
}
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// method exposed for event scheduler
|
2016-06-20 20:10:12 +00:00
|
|
|
function trimMessageAreasScheduledEvent(args, cb) {
|
2018-01-15 19:22:11 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
function trimMessageAreaByMaxMessages(areaInfo, cb) {
|
|
|
|
if(0 === areaInfo.maxMessages) {
|
|
|
|
return cb(null);
|
|
|
|
}
|
2016-06-20 20:10:12 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
msgDb.run(
|
|
|
|
`DELETE FROM message
|
2018-06-23 03:26:46 +00:00
|
|
|
WHERE message_id IN(
|
|
|
|
SELECT message_id
|
|
|
|
FROM message
|
|
|
|
WHERE area_tag = ?
|
|
|
|
ORDER BY message_id DESC
|
|
|
|
LIMIT -1 OFFSET ${areaInfo.maxMessages}
|
|
|
|
);`,
|
2018-06-22 05:15:04 +00:00
|
|
|
[ areaInfo.areaTag.toLowerCase() ],
|
2018-06-23 03:26:46 +00:00
|
|
|
function result(err) { // no arrow func; need this
|
2018-06-22 05:15:04 +00:00
|
|
|
if(err) {
|
|
|
|
Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area');
|
|
|
|
} else {
|
|
|
|
Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully');
|
|
|
|
}
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function trimMessageAreaByMaxAgeDays(areaInfo, cb) {
|
|
|
|
if(0 === areaInfo.maxAgeDays) {
|
|
|
|
return cb(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
msgDb.run(
|
|
|
|
`DELETE FROM message
|
2018-06-23 03:26:46 +00:00
|
|
|
WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`,
|
2018-06-22 05:15:04 +00:00
|
|
|
[ areaInfo.areaTag ],
|
2018-06-23 03:26:46 +00:00
|
|
|
function result(err) { // no arrow func; need this
|
2018-06-22 05:15:04 +00:00
|
|
|
if(err) {
|
|
|
|
Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area');
|
|
|
|
} else {
|
|
|
|
Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays', count : this.changes }, 'Area trimmed successfully');
|
|
|
|
}
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function getAreaTags(callback) {
|
|
|
|
const areaTags = [];
|
|
|
|
|
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// We use SQL here vs API such that no-longer-used tags are picked up
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
|
|
|
msgDb.each(
|
|
|
|
`SELECT DISTINCT area_tag
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM message;`,
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, row) => {
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// We treat private mail special
|
2018-06-22 05:15:04 +00:00
|
|
|
if(!Message.isPrivateAreaTag(row.area_tag)) {
|
|
|
|
areaTags.push(row.area_tag);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
return callback(err, areaTags);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
function prepareAreaInfo(areaTags, callback) {
|
|
|
|
let areaInfos = [];
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// determine maxMessages & maxAgeDays per area
|
2018-06-22 05:15:04 +00:00
|
|
|
const config = Config();
|
|
|
|
areaTags.forEach(areaTag => {
|
|
|
|
|
|
|
|
let maxMessages = config.messageAreaDefaults.maxMessages;
|
2018-06-23 03:26:46 +00:00
|
|
|
let maxAgeDays = config.messageAreaDefaults.maxAgeDays;
|
2018-06-22 05:15:04 +00:00
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here
|
2018-06-22 05:15:04 +00:00
|
|
|
if(area) {
|
|
|
|
maxMessages = area.maxMessages || maxMessages;
|
2018-06-23 03:26:46 +00:00
|
|
|
maxAgeDays = area.maxAgeDays || maxAgeDays;
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
areaInfos.push( {
|
2018-06-23 03:26:46 +00:00
|
|
|
areaTag : areaTag,
|
|
|
|
maxMessages : maxMessages,
|
|
|
|
maxAgeDays : maxAgeDays,
|
2018-06-22 05:15:04 +00:00
|
|
|
} );
|
|
|
|
});
|
|
|
|
|
|
|
|
return callback(null, areaInfos);
|
|
|
|
},
|
|
|
|
function trimGeneralAreas(areaInfos, callback) {
|
|
|
|
async.each(
|
|
|
|
areaInfos,
|
|
|
|
(areaInfo, next) => {
|
|
|
|
trimMessageAreaByMaxMessages(areaInfo, err => {
|
|
|
|
if(err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
trimMessageAreaByMaxAgeDays(areaInfo, err => {
|
|
|
|
return next(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
},
|
|
|
|
function trimExternalPrivateSentMail(callback) {
|
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// *External* (FTN, email, ...) outgoing is cleaned up *after export*
|
|
|
|
// if it is older than the configured |maxExternalSentAgeDays| days
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
2018-06-23 03:26:46 +00:00
|
|
|
// Outgoing externally exported private mail is:
|
|
|
|
// - In the 'private_mail' area
|
|
|
|
// - Marked exported (state_flags0 exported bit set)
|
|
|
|
// - Marked with any external flavor (we don't mark local)
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
|
|
|
const maxExternalSentAgeDays = _.get(
|
|
|
|
Config,
|
|
|
|
'messageConferences.system_internal.areas.private_mail.maxExternalSentAgeDays',
|
|
|
|
30
|
|
|
|
);
|
|
|
|
|
|
|
|
msgDb.run(
|
|
|
|
`DELETE FROM message
|
2018-06-23 03:26:46 +00:00
|
|
|
WHERE message_id IN (
|
|
|
|
SELECT m.message_id
|
|
|
|
FROM message m
|
|
|
|
JOIN message_meta mms
|
|
|
|
ON m.message_id = mms.message_id AND
|
|
|
|
(mms.meta_category='System' AND mms.meta_name='${Message.SystemMetaNames.StateFlags0}' AND (mms.meta_value & ${Message.StateFlags0.Exported} = ${Message.StateFlags0.Exported}))
|
|
|
|
JOIN message_meta mmf
|
|
|
|
ON m.message_id = mmf.message_id AND
|
|
|
|
(mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}')
|
|
|
|
WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days')
|
|
|
|
);`,
|
|
|
|
function results(err) { // no arrow func; need this
|
2018-06-22 05:15:04 +00:00
|
|
|
if(err) {
|
|
|
|
Log.warn( { error : err.message }, 'Error trimming private externally sent messages');
|
|
|
|
} else {
|
|
|
|
Log.debug( { count : this.changes }, 'Private externally sent messages trimmed successfully');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return callback(null);
|
|
|
|
}
|
|
|
|
],
|
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
2016-02-17 05:11:55 +00:00
|
|
|
}
|