Merge branch '418-inactive-accounts-boards-handling' into develop

indiachan-spamvector
Thomas Lynch 2 years ago
commit 5870e35ec3
  1. 8
      configs/template.js.example
  2. 7
      controllers/forms/globalsettings.js
  3. 26
      db/accounts.js
  4. 44
      db/boards.js
  5. 3
      lib/misc/socketio.js
  6. 8
      lib/redis/redis.js
  7. 16
      migrations/0.8.0.js
  8. 3
      models/forms/changeglobalsettings.js
  9. 2
      package.json
  10. 44
      schedules/tasks/abandonedboards.js
  11. 98
      schedules/tasks/inactiveaccounts.js
  12. 16
      views/pages/globalmanagesettings.pug

@ -175,6 +175,14 @@ module.exports = {
//how many of the most recent newsposts to show on the homepage
maxRecentNews: 5,
//time for an account to be considered inactive
inactiveAccountTime: 7889400000,
//action for inactive accounts: nothing, forfeit staff positions, delete account
inactiveAccountAction: 0,
//action for handling abandoned boards (no board owner): nothing, lock, lock + unlist, delete board
abandonedBoardAction: 0,
/* filter filenames on posts and banners
false=no filtering
true=allow only A-Za-z0-9_-

@ -11,9 +11,9 @@ const changeGlobalSettings = require(__dirname+'/../../models/forms/changeglobal
module.exports = {
paramConverter: paramConverter({
timeFields: ['ban_duration', 'board_defaults_filter_ban_duration', 'default_ban_duration', 'block_bypass_expire_after_time', 'dnsbl_cache_time', 'board_defaults_delete_protection_age'],
timeFields: ['inactive_account_time', 'ban_duration', 'board_defaults_filter_ban_duration', 'default_ban_duration', 'block_bypass_expire_after_time', 'dnsbl_cache_time', 'board_defaults_delete_protection_age'],
trimFields: ['allowed_hosts', 'dnsbl_blacklists', 'other_mime_types', 'highlight_options_language_subset', 'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'archive_links', 'reverse_links'],
numberFields: ['filter_mode', 'auth_level',
numberFields: ['inactive_account_action', 'abandoned_board_action','filter_mode', 'auth_level',
'captcha_options_generate_limit', 'captcha_options_grid_size', 'captcha_options_grid_image_size', 'captcha_options_num_distorts_min', 'captcha_options_num_distorts_max',
'captcha_options_distortion', 'captcha_options_grid_icon_y_offset', 'flood_timers_same_content_same_ip', 'flood_timers_same_content_any_ip', 'flood_timers_any_content_same_ip',
'block_bypass_expire_after_uses', 'rate_limit_cost_captcha', 'rate_limit_cost_board_settings', 'rate_limit_cost_edit_post',
@ -68,6 +68,9 @@ module.exports = {
}
return false;
}, expected: true, error: 'Invalid reverse image search links URL format, must be a link containing %s where the url param belongs.' },
{ result: numberBody(req.body.inactive_account_time), expected: true, error: 'Invalid inactive account time' },
{ result: numberBody(req.body.inactive_account_action, 0, 2), expected: true, error: 'Inactive account action must be a number from 0-2' },
{ result: numberBody(req.body.abandoned_board_action, 0, 3), expected: true, error: 'Abandoned board action must be a number from 0-3' },
{ result: lengthBody(req.body.global_announcement, 0, 10000), expected: false, error: 'Global announcement must not exceed 10000 characters' },
{ result: lengthBody(req.body.filters, 0, 50000), expected: false, error: 'Filter text cannot exceed 50000 characters' },
{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: 'Filter mode must be a number from 0-2' },

@ -3,7 +3,8 @@
const Mongo = require(__dirname+'/db.js')
, db = Mongo.db.collection('accounts')
, bcrypt = require('bcrypt')
, cache = require(__dirname+'/../lib/redis/redis.js');
, cache = require(__dirname+'/../lib/redis/redis.js')
, { MONTH } = require(__dirname+'/../lib/converter/timeutils.js');
module.exports = {
@ -97,6 +98,14 @@ module.exports = {
});
},
getInactive: (duration=(MONTH*3)) => {
return db.find({
'lastActiveDate': {
'$lt': new Date(Date.now() - duration),
},
}).toArray();
},
find: (filter, skip=0, limit=0) => {
return db.find(filter, {
'projection': {
@ -175,6 +184,21 @@ module.exports = {
return res;
},
clearStaffAndOwnedBoards: async (usernames) => {
const res = await db.updateMany({
'_id': {
'$in': usernames
}
}, {
'$set': {
'staffBoards': [],
'ownedBoards': [],
}
});
cache.del(usernames.map(n => `users:${n}`));
return res;
},
getOwnedOrStaffBoards: (usernames) => {
return db.find({
'_id': {

@ -4,7 +4,8 @@ const Mongo = require(__dirname+'/db.js')
, cache = require(__dirname+'/../lib/redis/redis.js')
, dynamicResponse = require(__dirname+'/../lib/misc/dynamic.js')
, escapeRegExp = require(__dirname+'/../lib/input/escaperegexp.js')
, db = Mongo.db.collection('boards');
, db = Mongo.db.collection('boards')
, config = require(__dirname+'/../lib/misc/config.js');
module.exports = {
@ -309,6 +310,47 @@ module.exports = {
}
}).toArray();
},
getAbandoned: (action=0) => {
const filter = {
'webring': false,
'owner': null,
};
if (action === 1) {
//if just locking, only match unlocked boards
filter['settings.lockMode'] = { '$lt': 2 };
} else if (action === 2) {
//if locking+unlisting, match ones that satisfy any of the conditions
filter['$or'] = [
{ 'settings.unlistedWebring': false },
{ 'settings.unlistedLocal': false },
{ 'settings.lockMode': { '$lt': 2 } },
];
}
//else we return boards purely based on owner: null because they are going to be deleted anyway
return db
.find(filter)
.toArray();
},
unlistMany: (boards) => {
const update = {
'settings.lockMode': 2,
};
if (config.get.abandonedBoardAction === 2) {
update['settings.unlistedLocal'] = true;
update['settings.unlistedWebring'] = true;
}
cache.srem('boards:listed', boards);
cache.del(boards.map(b => `board:${b}`));
return db.updateMany({
'_id': {
'$in': boards,
},
}, {
'$set': update,
});
},
count: (filter, showSensitive=false, webringSites=false) => {
const addedFilter = {};

@ -80,6 +80,9 @@ module.exports = {
},
emitRoom: (room, event, message) => {
if (!module.exports.io) {
return; //not initialized or in process that doesnt emit these events
}
module.exports.io.to(room).emit(event, message);
},

@ -77,9 +77,9 @@ module.exports = {
return sharedClient.smembers(key);
},
//remove an item from a set
srem: (key, value) => {
return sharedClient.srem(key, value);
//remove item from a set
srem: (key, values) => {
return sharedClient.srem(key, values);
},
//get random item from set
@ -89,7 +89,7 @@ module.exports = {
//delete value with key
del: (keyOrKeys) => {
if (Array.isArray(keyOrKeys)) {
if (Array.isArray(keyOrKeys)) {
return sharedClient.del(...keyOrKeys);
} else {
return sharedClient.del(keyOrKeys);

@ -0,0 +1,16 @@
'use strict';
const timeUtils = require(__dirname+'/../lib/converter/timeutils.js');
module.exports = async(db, redis) => {
console.log('add inactive account and board auto handling');
await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
'$set': {
inactiveAccountTime: timeUtils.MONTH * 3,
inactiveAccountAction: 0, //no actions by default
abandonedBoardAction: 0,
},
});
console.log('Clearing globalsettings cache');
await redis.deletePattern('globalsettings');
};

@ -108,6 +108,9 @@ module.exports = async (req, res) => {
boardSettings: numberSetting(req.body.rate_limit_cost_board_settings, oldSettings.rateLimitCost.boardSettings),
editPost: numberSetting(req.body.rate_limit_cost_edit_post, oldSettings.rateLimitCost.editPost),
},
inactiveAccountTime: numberSetting(req.body.inactive_account_time, oldSettings.inactiveAccountTime),
inactiveAccountAction: numberSetting(req.body.inactive_account_action, oldSettings.inactiveAccountAction),
abandonedBoardAction: numberSetting(req.body.abandoned_board_action, oldSettings.abandonedBoardAction),
overboardLimit: numberSetting(req.body.overboard_limit, oldSettings.overboardLimit),
overboardCatalogLimit: numberSetting(req.body.overboard_catalog_limit, oldSettings.overboardCatalogLimit),
hotThreadsLimit: numberSetting(req.body.hot_threads_limit, oldSettings.hotThreadsLimit),

@ -1,7 +1,7 @@
{
"name": "jschan",
"version": "0.7.2",
"migrateVersion": "0.6.5",
"migrateVersion": "0.8.0",
"description": "",
"main": "server.js",
"dependencies": {

@ -0,0 +1,44 @@
'use strict';
const { debugLogs } = require(__dirname+'/../../configs/secrets.js')
, config = require(__dirname+'/../../lib/misc/config.js')
, { Boards } = require(__dirname+'/../../db/')
, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js')
, deleteBoard = require(__dirname+'/../../models/forms/deleteboard.js');
module.exports = {
func: async () => {
if (config.get.abandonedBoardAction === 0) {
return;
}
const abandonedBoards = await Boards.getAbandoned(config.get.abandonedBoardAction);
if (abandonedBoards.length === 0) {
return;
}
if (config.get.abandonedBoardAction <= 2) {
debugLogs && console.log(`Locking${config.get.abandonedBoardAction === 2 ? '+Unlisting' : ''} ${abandonedBoards.length} abandoned boards.`);
const abandonedURIs = abandonedBoards.map(b => b._id);
Boards.unlistMany(abandonedURIs);
} else { //must be 2
//run delete board model for each
debugLogs && console.log(`Deleting ${abandonedBoards.length} abandoned boards.`);
for (let board of abandonedBoards) {
try {
await deleteBoard(board._id, board);
} catch (err) {
debugLogs && console.log('Error deleting abandoned board:', err);
}
}
}
},
interval: timeUtils.DAY,
immediate: false,
condition: 'abandonedBoardAction'
};

@ -0,0 +1,98 @@
'use strict';
const { debugLogs } = require(__dirname+'/../../configs/secrets.js')
, config = require(__dirname+'/../../lib/misc/config.js')
, Redis = require(__dirname+'/../../lib/redis/redis.js')
, { Boards, Accounts } = require(__dirname+'/../../db/')
, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js');
module.exports = {
func: async () => {
if (config.get.inactiveAccountAction === 0) {
return;
}
const inactiveAccounts = await Accounts.getInactive(config.get.inactiveAccountTime);
if (inactiveAccounts.length === 0) {
return;
}
const cacheDeleteSet = new Set()
, boardBulkWrites = []
, inactiveWithBoards = inactiveAccounts.filter(acc => {
//only deal with boards if they have any (acc deletes still processed later)
return acc.ownedBoards.length > 0 || acc.staffBoards.length > 0;
});
let boardsPromise = null
, accountsPromise = null;
//create promise for boards (remove staff.${username})
inactiveWithBoards.forEach(acc => {
//remove account from staff and owner of all their boards
const accountBoards = [...acc.ownedBoards, ...acc.staffBoards];
accountBoards.forEach(b => cacheDeleteSet.add(b));
boardBulkWrites.push({
updateOne: {
filter: {
'_id': {
'$in': accountBoards,
//better to do per board for staff unsets? or per-account...
}
},
update: {
$unset: {
[`staff.${acc._id}`]: '',
},
}
}
});
acc.ownedBoards.forEach(ob => {
boardBulkWrites.push({
updateOne: {
filter: {
'_id': ob,
},
update: {
'$set': {
'owner': null,
},
}
}
});
});
});
if (boardBulkWrites.length > 0) {
boardsPromise = Boards.db.bulkWrite(boardBulkWrites);
}
//create promise for accounts (clearing staff positions or deleting fuly)
if (config.get.inactiveAccountAction === 2) {
debugLogs && console.log(`Deleting ${inactiveAccounts.length} inactive accounts`);
const inactiveUsernames = inactiveAccounts.map(acc => acc._id);
accountsPromise = Accounts.deleteMany(inactiveUsernames);
} else{
debugLogs && console.log(`Removing staff positions from ${inactiveWithBoards.length} inactive accounts`);
const inactiveUsernames = inactiveWithBoards.map(acc => acc._id);
accountsPromise = Accounts.clearStaffAndOwnedBoards(inactiveUsernames);
}
//execute promises
await Promise.all([
accountsPromise,
boardsPromise,
]);
//clear caches
cacheDeleteSet.forEach(b => Redis.del(`board:${b}`));
//users: cache already handled by Accounts.deleteMany or Accounts.clearStaffAndOwnedBoards
},
interval: timeUtils.DAY,
immediate: false,
condition: 'inactiveAccountAction'
};

@ -153,6 +153,22 @@ block content
.row
.label Max Homepage News Entries
input(type='number', name='max_recent_news', value=settings.maxRecentNews)
.row
.label Inactive Account Time
input(type='text', name='inactive_account_time', placeholder='e.g. 1w', value=settings.inactiveAccountTime)
.row
.label Inactive Account Action
select(name='inactive_account_action')
option(value='0', selected=settings.inactiveAccountTime === 0) Do nothing
option(value='1', selected=settings.inactiveAccountTime === 1) Forfeit staff positions
option(value='2', selected=settings.inactiveAccountTime === 2) Delete Account
.row
.label Abandoned Board Action
select(name='abandoned_board_action')
option(value='0', selected=settings.abandonedBoardAction === 0) Do nothing
option(value='1', selected=settings.abandonedBoardAction === 1) Lock board
option(value='2', selected=settings.abandonedBoardAction === 2) Lock+unlist board
option(value='3', selected=settings.abandonedBoardAction === 3) Delete board
.col.mr-5

Loading…
Cancel
Save