Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 190 additions & 171 deletions src/user/settings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

'use strict';

const validator = require('validator');
Expand All @@ -10,188 +9,208 @@
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(/&#x2F;/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(/&#x2F;/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) {

Check failure on line 133 in src/user/settings.js

View workflow job for this annotation

GitHub Actions / test

This line has a length of 124. Maximum allowed is 120
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;
};
Loading