diff --git a/src/user/settings.js b/src/user/settings.js index 48b9a8a491..57597aefbb 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -1,4 +1,3 @@ - 'use strict'; const validator = require('validator'); @@ -10,188 +9,208 @@ const plugins = require('../plugins'); const notifications = require('../notifications'); const languages = require('../languages'); -module.exports = function (User) { - const spiderDefaultSettings = { - usePagination: 1, - topicPostSort: 'oldest_to_newest', - postsPerPage: 20, - topicsPerPage: 20, - }; - const remoteDefaultSettings = Object.freeze({ - categoryWatchState: 'notwatching', - }); - - User.getSettings = async function (uid) { - if (parseInt(uid, 10) <= 0) { - const isSpider = parseInt(uid, 10) === -1; - return await onSettingsLoaded(uid, isSpider ? spiderDefaultSettings : {}); - } - let settings = await db.getObject(`user:${uid}:settings`); - settings = settings || {}; - settings.uid = uid; - return await onSettingsLoaded(uid, settings); - }; +const spiderDefaultSettings = { + usePagination: 1, + topicPostSort: 'oldest_to_newest', + postsPerPage: 20, + topicsPerPage: 20, +}; - User.getMultipleUserSettings = async function (uids) { - if (!Array.isArray(uids) || !uids.length) { - return []; - } +const remoteDefaultSettings = Object.freeze({ + categoryWatchState: 'notwatching', +}); + +const BOOLEAN_SETTINGS = Object.freeze({ + showemail: 0, + showfullname: 0, + openOutgoingLinksInNewTab: 0, + usePagination: 0, + followTopicsOnCreate: 1, + followTopicsOnReply: 0, + disableIncomingChats: 0, + topicSearchEnabled: 0, + updateUrlWithPostIndex: 1, + scrollToMyPost: 1, +}); + +const STRING_SETTINGS = Object.freeze({ + dailyDigestFreq: 'off', + topicPostSort: 'oldest_to_newest', + categoryTopicSort: 'recently_replied', + upvoteNotifFreq: 'all', + categoryWatchState: 'notwatching', +}); + +function parseJSONSetting(value, defaultValue) { + try { + return JSON.parse(value); + } catch (err) { + return defaultValue; + } +} + +function getSetting(settings, key, defaultValue) { + if (settings[key] || settings[key] === 0) { + return settings[key]; + } else if (activitypub.helpers.isUri(settings.uid) && remoteDefaultSettings[key]) { + return remoteDefaultSettings[key]; + } else if (meta.config[key] || meta.config[key] === 0) { + return meta.config[key]; + } + return defaultValue; +} - const keys = uids.map(uid => `user:${uid}:settings`); - let settings = await db.getObjects(keys); - settings = settings.map((userSettings, index) => { - userSettings = userSettings || {}; - userSettings.uid = uids[index]; - return userSettings; - }); - return await Promise.all(settings.map(s => onSettingsLoaded(s.uid, s))); - }; +async function onSettingsLoaded(uid, settings) { + const data = await plugins.hooks.fire('filter:user.getSettings', { uid: uid, settings: settings }); + settings = data.settings; - async function onSettingsLoaded(uid, settings) { - const data = await plugins.hooks.fire('filter:user.getSettings', { uid: uid, settings: settings }); - settings = data.settings; - - const defaultTopicsPerPage = meta.config.topicsPerPage; - const defaultPostsPerPage = meta.config.postsPerPage; - - settings.showemail = parseInt(getSetting(settings, 'showemail', 0), 10) === 1; - settings.showfullname = parseInt(getSetting(settings, 'showfullname', 0), 10) === 1; - settings.openOutgoingLinksInNewTab = parseInt(getSetting(settings, 'openOutgoingLinksInNewTab', 0), 10) === 1; - settings.dailyDigestFreq = getSetting(settings, 'dailyDigestFreq', 'off'); - settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1; - settings.topicsPerPage = Math.min( - meta.config.maxTopicsPerPage, - settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, - defaultTopicsPerPage - ); - settings.postsPerPage = Math.min( - meta.config.maxPostsPerPage, - settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, - defaultPostsPerPage - ); - settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB'; - settings.acpLang = settings.acpLang || settings.userLang; - settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest'); - settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'recently_replied'); - settings.followTopicsOnCreate = parseInt(getSetting(settings, 'followTopicsOnCreate', 1), 10) === 1; - settings.followTopicsOnReply = parseInt(getSetting(settings, 'followTopicsOnReply', 0), 10) === 1; - settings.upvoteNotifFreq = getSetting(settings, 'upvoteNotifFreq', 'all'); - settings.disableIncomingChats = parseInt(getSetting(settings, 'disableIncomingChats', 0), 10) === 1; - settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; - settings.updateUrlWithPostIndex = parseInt(getSetting(settings, 'updateUrlWithPostIndex', 1), 10) === 1; - settings.bootswatchSkin = validator.escape(String(settings.bootswatchSkin || '')); - settings.homePageRoute = validator.escape(String(settings.homePageRoute || '')).replace(///g, '/'); - settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; - settings.categoryWatchState = getSetting(settings, 'categoryWatchState', 'notwatching'); - - const notificationTypes = await notifications.getAllNotificationTypes(); - notificationTypes.forEach((notificationType) => { - settings[notificationType] = getSetting(settings, notificationType, 'notification'); - }); - - settings.chatAllowList = parseJSONSetting(settings.chatAllowList || '[]', []).map(String); - settings.chatDenyList = parseJSONSetting(settings.chatDenyList || '[]', []).map(String); - return settings; + for (const [key, defaultVal] of Object.entries(BOOLEAN_SETTINGS)) { + settings[key] = parseInt(getSetting(settings, key, defaultVal), 10) === 1; + } + for (const [key, defaultVal] of Object.entries(STRING_SETTINGS)) { + settings[key] = getSetting(settings, key, defaultVal); } - function parseJSONSetting(value, defaultValue) { - try { - return JSON.parse(value); - } catch (err) { - return defaultValue; - } + const defaultTopicsPerPage = meta.config.topicsPerPage; + const defaultPostsPerPage = meta.config.postsPerPage; + settings.topicsPerPage = Math.min( + meta.config.maxTopicsPerPage, + settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, + defaultTopicsPerPage + ); + settings.postsPerPage = Math.min( + meta.config.maxPostsPerPage, + settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, + defaultPostsPerPage + ); + + settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB'; + settings.acpLang = settings.acpLang || settings.userLang; + settings.bootswatchSkin = validator.escape(String(settings.bootswatchSkin || '')); + settings.homePageRoute = validator.escape(String(settings.homePageRoute || '')).replace(///g, '/'); + + const notificationTypes = await notifications.getAllNotificationTypes(); + notificationTypes.forEach((notificationType) => { + settings[notificationType] = getSetting(settings, notificationType, 'notification'); + }); + + settings.chatAllowList = parseJSONSetting(settings.chatAllowList || '[]', []).map(String); + settings.chatDenyList = parseJSONSetting(settings.chatDenyList || '[]', []).map(String); + return settings; +} + +async function getSettings(uid) { + if (parseInt(uid, 10) <= 0) { + const isSpider = parseInt(uid, 10) === -1; + return await onSettingsLoaded(uid, isSpider ? spiderDefaultSettings : {}); } + let settings = await db.getObject(`user:${uid}:settings`); + settings = settings || {}; + settings.uid = uid; + return await onSettingsLoaded(uid, settings); +} + +async function getMultipleUserSettings(uids) { + if (!Array.isArray(uids) || !uids.length) { + return []; + } + const keys = uids.map(uid => `user:${uid}:settings`); + let settings = await db.getObjects(keys); + settings = settings.map((userSettings, index) => { + userSettings = userSettings || {}; + userSettings.uid = uids[index]; + return userSettings; + }); + return await Promise.all(settings.map(s => onSettingsLoaded(s.uid, s))); +} - function getSetting(settings, key, defaultValue) { - if (settings[key] || settings[key] === 0) { - return settings[key]; - } else if (activitypub.helpers.isUri(settings.uid) && remoteDefaultSettings[key]) { - return remoteDefaultSettings[key]; - } else if (meta.config[key] || meta.config[key] === 0) { - return meta.config[key]; - } - return defaultValue; +async function validatePagination(data) { + const maxPostsPerPage = meta.config.maxPostsPerPage || 20; + if (!data.postsPerPage || parseInt(data.postsPerPage, 10) <= 1 || parseInt(data.postsPerPage, 10) > maxPostsPerPage) { + throw new Error(`[[error:invalid-pagination-value, 2, ${maxPostsPerPage}]]`); + } + const maxTopicsPerPage = meta.config.maxTopicsPerPage || 20; + if (!data.topicsPerPage || parseInt(data.topicsPerPage, 10) <= 1 || parseInt(data.topicsPerPage, 10) > maxTopicsPerPage) { + throw new Error(`[[error:invalid-pagination-value, 2, ${maxTopicsPerPage}]]`); } + return { maxPostsPerPage, maxTopicsPerPage }; +} - User.saveSettings = async function (uid, data) { - const maxPostsPerPage = meta.config.maxPostsPerPage || 20; - if ( - !data.postsPerPage || - parseInt(data.postsPerPage, 10) <= 1 || - parseInt(data.postsPerPage, 10) > maxPostsPerPage - ) { - throw new Error(`[[error:invalid-pagination-value, 2, ${maxPostsPerPage}]]`); - } +async function validateLanguage(data) { + const languageCodes = await languages.listCodes(); + if (data.userLang && !languageCodes.includes(data.userLang)) { + throw new Error('[[error:invalid-language]]'); + } + if (data.acpLang && !languageCodes.includes(data.acpLang)) { + throw new Error('[[error:invalid-language]]'); + } + data.userLang = data.userLang || meta.config.defaultLang; +} + +async function saveSettings(uid, data) { + const { maxPostsPerPage, maxTopicsPerPage } = await validatePagination(data); + await validateLanguage(data); + + plugins.hooks.fire('action:user.saveSettings', { uid: uid, settings: data }); + + const settings = { + showemail: data.showemail, + showfullname: data.showfullname, + openOutgoingLinksInNewTab: data.openOutgoingLinksInNewTab, + dailyDigestFreq: data.dailyDigestFreq || 'off', + usePagination: data.usePagination, + topicsPerPage: Math.min(data.topicsPerPage, parseInt(maxTopicsPerPage, 10) || 20), + postsPerPage: Math.min(data.postsPerPage, parseInt(maxPostsPerPage, 10) || 20), + userLang: data.userLang || meta.config.defaultLang, + acpLang: data.acpLang || meta.config.defaultLang, + followTopicsOnCreate: data.followTopicsOnCreate, + followTopicsOnReply: data.followTopicsOnReply, + disableIncomingChats: data.disableIncomingChats, + topicSearchEnabled: data.topicSearchEnabled, + updateUrlWithPostIndex: data.updateUrlWithPostIndex, + homePageRoute: ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''), + scrollToMyPost: data.scrollToMyPost, + upvoteNotifFreq: data.upvoteNotifFreq, + bootswatchSkin: data.bootswatchSkin, + categoryWatchState: data.categoryWatchState, + categoryTopicSort: data.categoryTopicSort, + topicPostSort: data.topicPostSort, + chatAllowList: data.chatAllowList, + chatDenyList: data.chatDenyList, + }; - const maxTopicsPerPage = meta.config.maxTopicsPerPage || 20; - if ( - !data.topicsPerPage || - parseInt(data.topicsPerPage, 10) <= 1 || - parseInt(data.topicsPerPage, 10) > maxTopicsPerPage - ) { - throw new Error(`[[error:invalid-pagination-value, 2, ${maxTopicsPerPage}]]`); + const notificationTypes = await notifications.getAllNotificationTypes(); + notificationTypes.forEach((notificationType) => { + if (data[notificationType]) { + settings[notificationType] = data[notificationType]; } + }); - const languageCodes = await languages.listCodes(); - if (data.userLang && !languageCodes.includes(data.userLang)) { - throw new Error('[[error:invalid-language]]'); - } - if (data.acpLang && !languageCodes.includes(data.acpLang)) { - throw new Error('[[error:invalid-language]]'); - } - data.userLang = data.userLang || meta.config.defaultLang; - - plugins.hooks.fire('action:user.saveSettings', { uid: uid, settings: data }); - - const settings = { - showemail: data.showemail, - showfullname: data.showfullname, - openOutgoingLinksInNewTab: data.openOutgoingLinksInNewTab, - dailyDigestFreq: data.dailyDigestFreq || 'off', - usePagination: data.usePagination, - topicsPerPage: Math.min(data.topicsPerPage, parseInt(maxTopicsPerPage, 10) || 20), - postsPerPage: Math.min(data.postsPerPage, parseInt(maxPostsPerPage, 10) || 20), - userLang: data.userLang || meta.config.defaultLang, - acpLang: data.acpLang || meta.config.defaultLang, - followTopicsOnCreate: data.followTopicsOnCreate, - followTopicsOnReply: data.followTopicsOnReply, - disableIncomingChats: data.disableIncomingChats, - topicSearchEnabled: data.topicSearchEnabled, - updateUrlWithPostIndex: data.updateUrlWithPostIndex, - homePageRoute: ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''), - scrollToMyPost: data.scrollToMyPost, - upvoteNotifFreq: data.upvoteNotifFreq, - bootswatchSkin: data.bootswatchSkin, - categoryWatchState: data.categoryWatchState, - categoryTopicSort: data.categoryTopicSort, - topicPostSort: data.topicPostSort, - chatAllowList: data.chatAllowList, - chatDenyList: data.chatDenyList, - }; - const notificationTypes = await notifications.getAllNotificationTypes(); - notificationTypes.forEach((notificationType) => { - if (data[notificationType]) { - settings[notificationType] = data[notificationType]; - } - }); - const result = await plugins.hooks.fire('filter:user.saveSettings', { uid: uid, settings: settings, data: data }); - await db.setObject(`user:${uid}:settings`, result.settings); - await User.updateDigestSetting(uid, data.dailyDigestFreq); - return await User.getSettings(uid); - }; + const result = await plugins.hooks.fire('filter:user.saveSettings', { uid: uid, settings: settings, data: data }); + await db.setObject(`user:${uid}:settings`, result.settings); + await updateDigestSetting(uid, data.dailyDigestFreq); + return await getSettings(uid); +} - User.updateDigestSetting = async function (uid, dailyDigestFreq) { - await db.sortedSetsRemove(['digest:day:uids', 'digest:week:uids', 'digest:month:uids'], uid); - if (['day', 'week', 'biweek', 'month'].includes(dailyDigestFreq)) { - await db.sortedSetAdd(`digest:${dailyDigestFreq}:uids`, Date.now(), uid); - } - }; +async function updateDigestSetting(uid, dailyDigestFreq) { + await db.sortedSetsRemove(['digest:day:uids', 'digest:week:uids', 'digest:month:uids'], uid); + if (['day', 'week', 'biweek', 'month'].includes(dailyDigestFreq)) { + await db.sortedSetAdd(`digest:${dailyDigestFreq}:uids`, Date.now(), uid); + } +} - User.setSetting = async function (uid, key, value) { - if (parseInt(uid, 10) <= 0) { - return; - } +async function setSetting(uid, key, value) { + if (parseInt(uid, 10) <= 0) { + return; + } + await db.setObjectField(`user:${uid}:settings`, key, value); +} - await db.setObjectField(`user:${uid}:settings`, key, value); - }; -}; +// Factory just wires implementations onto the User object — no logic here +module.exports = function (User) { + User.getSettings = getSettings; + User.getMultipleUserSettings = getMultipleUserSettings; + User.saveSettings = saveSettings; + User.updateDigestSetting = updateDigestSetting; + User.setSetting = setSetting; +}; \ No newline at end of file