Merge branch 'develop' into '469-captcha-improvements'

# Conflicts:
#   controllers/forms/globalsettings.js
#   gulpfile.js
#   migrations/0.8.0.js
#   package.json
indiachan-spamvector
Thomas Lynch 2 years ago
commit 5c359a8bda
  1. 5
      .eslintrc.json
  2. 1
      .gitlab-ci.yml
  3. 14
      CHANGELOG.md
  4. 2
      README.md
  5. 8
      configs/template.js.example
  6. 17
      controllers/forms/globalsettings.js
  7. 26
      db/accounts.js
  8. 44
      db/boards.js
  9. 10
      gulp/res/js/filters.js
  10. 67
      gulp/res/js/ptchina-playlist.js
  11. 2
      gulpfile.js
  12. 4
      lib/captcha/getdistorts.js
  13. 3
      lib/middleware/file/filemiddlewares.js
  14. 3
      lib/misc/socketio.js
  15. 8
      lib/redis/redis.js
  16. 9
      migrations/0.8.0.js
  17. 3
      models/forms/changeglobalsettings.js
  18. 28
      package-lock.json
  19. 2
      package.json
  20. 44
      schedules/tasks/abandonedboards.js
  21. 98
      schedules/tasks/inactiveaccounts.js
  22. 1
      views/mixins/post.pug
  23. 16
      views/pages/globalmanagesettings.pug

@ -28,6 +28,11 @@
"ecmaVersion": "latest"
},
"rules": {
"brace-style": [
"error",
"1tbs",
{ "allowSingleLine": true }
],
"indent": [
"error",
"tab",

@ -30,6 +30,7 @@ unit-tests:
- 'npm install jest -g'
- 'cp configs/secrets.js.example configs/secrets.js'
- 'jest --ci --verbose --reporters=default --reporters=jest-junit --testPathIgnorePatterns=./test/ --collectCoverage --coverageReporters text --coverageReporters cobertura'
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
when: always
reports:

@ -1,3 +1,17 @@
### 0.8.0
- Add a new option for automatically forfeiting board staff position and/or deleting inactive accounts with customisable time.
- Add a new option to automatically lock, lock+unlist, or delete boards that have no board owner.
- Add unofficial Typescript SDK + typings for jschan api to README, (thanks to Michell/ussaohelcim).
- Add "playlist" button in OP dropdown to download thread audio/video as m3u playlist (thanks to Michell/ussaohelcim).
### 0.7.3
- Bugfix some captcha generations causing server error due non-integer argument to randomRange.
### 0.7.2
- Add playlist generating bookmarklet script to README
- Bugfix some captcha generations causing server error due to bad random number range.
- Fix UTF8-filenames not being decoded corectly (busboy made this not the default in a recent update).
### 0.7.1
- Show sticky level on hover (title property) of pin icon.
- Reduce frontend script size by ~10KB. More improvement to come in future updates.

@ -36,6 +36,8 @@ Interested in contributing to jschan development? See [CONTRIBUTING.md](CONTRIBU
## Related Projects
Here are some other projects related to jschan that you might find useful. Unless explicitly specified here, they are not officially endorsed or otherwise guaranteed to work or be safe and should be used at your own risk.
- [myumyu/globalafk](https://gitgud.io/myumyu/globalafk/) - "A simple python script that sends ugly notifications when something happens on a jschan imageboard that you moderate."
- [ussaohelcim/jschan-api-sdk](https://github.com/ussaohelcim/jschan-api-sdk) - JavaScript/TypeScript SDK for jschan.
- [ussaohelcim/jschan-api-types](https://github.com/ussaohelcim/jschan-api-types) - TypeScript typings for jschan API.
## For generous people

@ -187,6 +187,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_-

@ -12,13 +12,13 @@ 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'],
trimFields: ['captcha_options_grid_question', 'captcha_options_grid_trues', 'captcha_options_grid_falses','allowed_hosts', 'dnsbl_blacklists', 'other_mime_types',
'highlight_options_language_subset', 'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'archive_links', 'reverse_links', 'captcha_options_text_font'],
numberFields: ['filter_mode', 'auth_level', 'captcha_options_text_wave', 'captcha_options_text_paint', 'captcha_options_text_noise', 'captcha_options_grid_noise', 'captcha_options_grid_edge',
'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',
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: ['captcha_options_grid_question', 'captcha_options_grid_trues', 'captcha_options_grid_falses', 'captcha_options_text_font', '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: ['inactive_account_action', 'abandoned_board_action', 'filter_mode', 'auth_level', 'captcha_options_text_wave', 'captcha_options_text_paint', 'captcha_options_text_noise',
'captcha_options_grid_noise', 'captcha_options_grid_edge', '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',
'overboard_limit', 'hot_threads_limit', 'hot_threads_threshold', 'overboard_catalog_limit', 'lock_wait', 'prune_modlogs', 'prune_ips', 'thumb_size', 'video_thumb_percentage', 'quote_limit', 'preview_replies',
'sticky_preview_replies', 'early_404_fraction', 'early_404_replies', 'max_recent_news', 'highlight_options_threshold', 'global_limits_thread_limit_min',
'global_limits_thread_limit_max', 'global_limits_reply_limit_min', 'global_limits_reply_limit_max', 'global_limits_bump_limit_min', 'global_limits_bump_limit_max',
@ -70,6 +70,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 = {};

@ -271,6 +271,16 @@ const postMenuChange = function() {
threadWatcher.add(postDataset.board, postDataset.postId, { subject: watcherSubject, unread: 0, updatedDate: new Date() });
return;
}
case 'playlist':{
console.log('creating playlist...');
window.dispatchEvent(new CustomEvent('createPlaylist', {
detail:{
board:postDataset.board,
postId:postDataset.postId
}
}));
break;
}
}
toggleFilter(filterType, filterData, hiding);
};

@ -0,0 +1,67 @@
//https://github.com/ussaohelcim/ptchina-playlist/tree/bookmarklet-let
async function threadToPlaylist(board, postId) {
async function getThread() {
let link = `${window.location.origin}/${board}/thread/${postId}.json`;
return await fetch(link).then(res => res.json());
}
function isAudioOrVideo(file) {
const mimeTypes = ['video', 'audio'];
const fileType = file.mimetype.split('/')[0];
return fileType === mimeTypes[0] || fileType === mimeTypes[1];
}
async function getMedia(thread) {
const files = [];
thread.files.forEach((file) => {
if (isAudioOrVideo(file)) {
files.push(file);
}
});
for (let i = 0; i < thread.replies.length; i++) {
const replyFiles = thread.replies[i].files;
replyFiles.forEach((file) => {
if (isAudioOrVideo(file)) {
files.push(file);
}
});
}
return files;
}
function createPlaylist(medias) {
const lines = [];
lines.push('#EXTM3U');
for (let i = 0; i < medias.length; i++) {
const media = medias[i];
lines.push(`#EXTINF:${media.duration}, ${media.originalFilename}`);
lines.push(`${location.origin}/file/${media.filename}`);
}
let playlist = lines.join('\n');
return playlist;
}
function downloadPlaylist(filename, playlist) {
const blob = new Blob([playlist], { type: 'application/mpegurl' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
try {
const thread = await getThread();
const files = await getMedia(thread);
const playlist = await createPlaylist(files);
if (playlist.split('\n').length > 1) {
downloadPlaylist(`${thread.board}-${thread.postId}.m3u`, playlist);
} else {
console.log('No video/audio files in this thread.');
}
} catch (error) {
console.log(error);
}
}
window.addEventListener('createPlaylist', (e) => {
threadToPlaylist(e.detail.board, e.detail.postId);
});

@ -156,7 +156,7 @@ async function wipe() {
const defaultConfig = require(__dirname+'/configs/template.js.example');
await Mongo.setConfig(defaultConfig);
const collectionNames = ['accounts', 'bans', 'custompages', 'boards', 'captcha', 'files',
'modlog','news', 'posts', 'poststats', 'ratelimit', 'bypass', 'roles'];
for (const name of collectionNames) {

@ -13,8 +13,8 @@ module.exports = async (width, height, numDistorts, distortion) => {
for (let i = 0; i < randNumDistorts; i++) {
//start and end of divided width
const divStart = (div * i)
, divEnd = (div * (i + 1));
const divStart = Math.floor(div * i)
, divEnd = Math.floor(div * (i + 1));
//origin coordinate for distortion point
const originx = await randomRange(divStart, divEnd)

@ -20,7 +20,7 @@ const { debugLogs } = require(__dirname+'/../../../configs/secrets.js')
});
}
, updateHandlers = () => {
const { globalLimits, filterFileNames, spaceFileNameReplacement } = require(__dirname+'/../../misc/config.js').get;
const { globalLimits, filterFileNames, spaceFileNameReplacement } = require(__dirname+'/../../misc/config.js').get;
['flag', 'banner', 'asset', 'post'].forEach(fileType => {
const fileSizeLimit = globalLimits[`${fileType}FilesSize`];
const fileNumLimit = globalLimits[`${fileType}Files`];
@ -34,6 +34,7 @@ const { debugLogs } = require(__dirname+'/../../../configs/secrets.js')
});
};
fileHandlers[fileType] = upload({
defParamCharset: 'utf8',
debug: debugLogs,
createParentPath: true,
safeFileNames: filterFileNames,

@ -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);

@ -1,7 +1,9 @@
'use strict';
const timeUtils = require(__dirname+'/../lib/converter/timeutils.js');
module.exports = async(db, redis) => {
console.log('add more captcha options');
console.log('add more captcha options and add inactive account and board auto handling');
await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
'$set': {
'captchaOptions.text': {
@ -16,8 +18,11 @@ module.exports = async(db, redis) => {
'captchaOptions.grid.question': 'Select the solid/filled icons',
'captchaOptions.grid.noise': 0,
'captchaOptions.grid.edge': 25,
'inactiveAccountTime': timeUtils.MONTH * 3,
'inactiveAccountAction': 0, //no actions by default
'abandonedBoardAction': 0,
},
});
console.log('Clearing globalsettings cache');
await redis.deletePattern('globalsettings');
};
};

@ -120,6 +120,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),

28
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "jschan",
"version": "0.7.1",
"version": "0.7.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jschan",
"version": "0.7.1",
"version": "0.7.3",
"license": "AGPL-3.0-only",
"dependencies": {
"@fatchan/express-fileupload": "^1.4.0",
@ -3143,7 +3143,7 @@
"node_modules/chokidar/node_modules/glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
"dependencies": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
@ -5944,7 +5944,7 @@
"node_modules/glob-stream": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz",
"integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=",
"integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==",
"dependencies": {
"extend": "^3.0.0",
"glob": "^7.1.1",
@ -5964,7 +5964,7 @@
"node_modules/glob-stream/node_modules/glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
"dependencies": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
@ -8084,9 +8084,9 @@
}
},
"node_modules/jpeg-js": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz",
"integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q=="
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
},
"node_modules/js-git": {
"version": "0.7.8",
@ -16587,7 +16587,7 @@
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
@ -18771,7 +18771,7 @@
"glob-stream": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz",
"integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=",
"integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==",
"requires": {
"extend": "^3.0.0",
"glob": "^7.1.1",
@ -18788,7 +18788,7 @@
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
@ -20392,9 +20392,9 @@
}
},
"jpeg-js": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz",
"integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q=="
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
},
"js-git": {
"version": "0.7.8",

@ -1,6 +1,6 @@
{
"name": "jschan",
"version": "0.7.1",
"version": "0.8.0",
"migrateVersion": "0.8.0",
"description": "",
"main": "server.js",

@ -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'
};

@ -80,6 +80,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false, overboar
option(value='moderate') Moderate
if !post.thread
option(value='watch') Watch
option(value='playlist') Playlist
.post-data
if post.files.length > 0
.post-files(class=(post.files.length > 1 ? 'fn' : ''))

@ -154,6 +154,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