Inactive accounts handling schedule, globalsettings for it and migration.

Plus the same for abandoned boards handling, just still TODO the schedule.
ref #418
merge-requests/341/head
Thomas Lynch 2 years ago
parent 6d90f47a78
commit 3ab0a271c4
  1. 8
      configs/template.js.example
  2. 7
      controllers/forms/globalsettings.js
  3. 26
      db/accounts.js
  4. 16
      migrations/0.8.0.js
  5. 3
      models/forms/changeglobalsettings.js
  6. 2
      package.json
  7. 26
      schedules/tasks/abandonedboards.js
  8. 100
      schedules/tasks/inactiveaccounts.js
  9. 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, 3), expected: true, error: 'Inactive account action must be a number from 0-3' },
{ result: numberBody(req.body.abandoned_board_action, 0, 2), expected: true, error: 'Abandoned board action must be a number from 0-2' },
{ 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': {

@ -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,26 @@
'use strict';
const fetch = require('node-fetch')
, { 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.abandonedBoardAction === 0) {
return;
}
//TODO: handle abandoned boards. 1=unlist, 2=unlist+lock, 3=delete
},
interval: timeUtils.DAY,
immediate: false,
condition: 'abandonedBoardAction'
};

@ -0,0 +1,100 @@
'use strict';
const fetch = require('node-fetch')
, { 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 = []
, accountBulkWrites = []
, 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: true,
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