diff --git a/server/routes/profile.js b/server/routes/profile.js index 6a2d39e1a..94a742eca 100644 --- a/server/routes/profile.js +++ b/server/routes/profile.js @@ -67,7 +67,136 @@ const verifyLinks = (links, authenticators, username, linksMetadata) => { return verifiedLinks; } -const fetchProfiles = async (db, username, self) => { +class ProfileOptions { + default_props = { + enabled: true, + default_value: true + }; + locale_specific_props = { + enabled: true, + default_value: true + }; + + constructor(query_obj, locales) { + const default_props = query_obj["props"]; + this.default_props = ProfileOptions._parse_props(default_props) + + for (const key of Object.keys(locales)) { + const lprops_obj = query_obj["lprops"]; + let props; + if (typeof lprops_obj === "object") { + props = lprops_obj[key]; + } else { + props = query_obj[`lprops.${key.toLowerCase()}`]; + } + if (props != null) { + this.locale_specific_props[key] = ProfileOptions._parse_props(props); + } + } + } + + static _parse_props(raw_param) { + if (raw_param) { + /** @type {Array} */ + let properties; + let default_value = false; + const obj = { + default_value + }; + if (Array.isArray(raw_param)) { + properties = raw_param + } else if (typeof raw_param == "object") { + return { + ...raw_param, + enabled: true, + default_value: false, + } + } else if (typeof raw_param === 'string') { + if (raw_param === "none") { + // Don't show the locale at all + return { + enabled: false, + default_value: false + } + } else if (raw_param === "empty") { + // Include it as an object, but don't include any data + return { + enabled: true, + default_value: false + } + } else if (raw_param === "all") { + // All properties should be shown + return { + enabled: true, + default_value: true + } + } + properties = raw_param.split(','); + } + + for (const property of properties) { + obj[property] = !default_value; + } + + return { + ...obj, + enabled: true, + }; + } else { + return { + enabled: true, + default_value: true + } + } + } + + + _data_for(locale) { + return this.locale_specific_props[locale] ?? this.default_props; + } + + /** + * @param {string} locale + * @return {boolean} + */ + show_at_all(locale) { + return this._data_for(locale).enabled ?? true + } + /** + * @param {string} locale + * @param {string} property + * @return {boolean} + */ + prop(locale, property) { + const data = this._data_for(locale); + if (!(data.enabled ?? true)) return false; + return data[property] ?? data.default_value ?? true; + } + + /** + * @template T + * @param {string} locale + * @param {string} property + * @param {() => T} value + * @return {T | undefined} + */ + propv(locale, property, value) { + if (this.prop(locale, property) === true) { + return value(); + } else { + return undefined; + } + } +} + +/** + * @param db + * @param {string} username + * @param {boolean} self + * @param {ProfileOptions} opts + * @return {Promise<{}>} + */ +const fetchProfiles = async (db, username, self, opts) => { const user = await db.get(SQL`SELECT id FROM users WHERE usernameNorm = ${normalise(username)}`); if (!user) { return {}; @@ -90,6 +219,7 @@ const fetchProfiles = async (db, username, self) => { const p = {} for (let profile of profiles) { + if (!opts.show_at_all(profile)) continue; const links = JSON.parse(profile.links).map(l => normaliseUrl(l)).filter(l => !!l); const linksMetadata = {}; for (let link of await db.all(SQL`SELECT * FROM links WHERE url IN (`.append(links.map(k => `'${k.replace(/'/g, "''")}'`).join(',')).append(SQL`)`))) { @@ -99,39 +229,42 @@ const fetchProfiles = async (db, username, self) => { nodeinfo: JSON.parse(link.nodeinfo), }; } - const circle = await fetchCircles(db, profile.id, user.id); - p[profile.locale] = { - opinions: JSON.parse(profile.opinions), - names: JSON.parse(profile.names), - pronouns: JSON.parse(profile.pronouns), - description: profile.description, - age: calcAge(profile.birthday), - links, - linksMetadata, - verifiedLinks: verifyLinks(links, linkAuthenticators, username, linksMetadata), - flags: JSON.parse(profile.flags), - customFlags: JSON.parse(profile.customFlags), - words: JSON.parse(profile.words), - birthday: self ? profile.birthday : undefined, - timezone: profile.timezone ? JSON.parse(profile.timezone) : null, - teamName: profile.teamName, - footerName: profile.footerName, - footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [], - credentials: profile.credentials ? profile.credentials.split('|') : [], - credentialsLevel: profile.credentialsLevel, - credentialsName: profile.credentialsName, - card: profile.card, - cardDark: profile.cardDark, - circle, - sensitive: JSON.parse(profile.sensitive), + + const propv = (key, value) => opts.propv(profile, key, value); + + const profile_obj = { + opinions: propv("opinions", () => JSON.parse(profile.opinions)), + names: propv("names", () => JSON.parse(profile.names)), + pronouns: propv("pronouns", () => JSON.parse(profile.pronouns)), + description: propv("description", () => profile.description), + age: propv("age", () => calcAge(profile.birthday)), + links: propv("links", () => links), // todo: calculate link data only if needed + linksMetadata: propv("links", () => linksMetadata), + verifiedLinks: propv("links", () => verifyLinks(links, linkAuthenticators, username, linksMetadata)), + flags: propv("flags", () => JSON.parse(profile.flags)), + customFlags: propv("flags", () => JSON.parse(profile.customFlags)), + words: propv("words", () => JSON.parse(profile.words)), + birthday: propv("age", () => self ? profile.birthday : undefined), + timezone: propv("timezone", () => profile.timezone ? JSON.parse(profile.timezone) : null), + teamName: propv("team", () => profile.teamName), + footerName: propv("team", () => profile.footerName), + footerAreas: propv("team", () => profile.footerAreas ? profile.footerAreas.split(',') : []), + credentials: propv("credentials", () => profile.credentials ? profile.credentials.split('|') : []), + credentialsLevel: propv("credentials", () => profile.credentialsLevel), + credentialsName: propv("credentials", () => profile.credentialsName), + card: propv("card_image", () => profile.card), + cardDark: propv("card_image", () => profile.cardDark), + circle: await propv("circle", () => fetchCircles(db, profile.id, user.id)), + sensitive: propv("sensitive", () => JSON.parse(profile.sensitive)), }; + p[profile.locale] = profile_obj; } return p; }; -export const profilesSnapshot = async (db, username) => { - return JSON.stringify(await fetchProfiles(db, username, true), null, 4); +export const profilesSnapshot = async (db, username, opts) => { + return JSON.stringify(await fetchProfiles(db, username, true, opts), null, 4); } const susRegexes = fs.readFileSync(__dirname + '/../../moderation/sus.txt').toString('utf-8').split('\n').filter(x => !!x); @@ -229,9 +362,15 @@ const fetchCircles = async(db, profileId, userId) => { const router = Router(); +/** + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {any} user + */ const fetchProfilesRoute = async (req, res, user) => { const isSelf = req.user && req.user.username === req.params.username; const isAdmin = req.isGranted('users'); + const opts = new ProfileOptions(req.query, req.locales); if (!user || (user.bannedReason !== null && !isAdmin && !isSelf)) { return res.json({ @@ -245,7 +384,7 @@ const fetchProfilesRoute = async (req, res, user) => { user.bannedTerms = user.bannedTerms ? user.bannedTerms.split(',') : []; - let profiles = await fetchProfiles(req.db, user.username, isSelf); + let profiles = await fetchProfiles(req.db, user.username, isSelf, opts); if (req.query.version !== '2') { for (let [locale, profile] of Object.entries(profiles)) { profiles[locale] = downgradeToV1(profile); @@ -569,7 +708,7 @@ router.post('/profile/delete/:locale', handleErrorAsync(async (req, res) => { await req.db.get(SQL`DELETE FROM profiles WHERE userId = ${req.user.id} AND locale = ${req.params.locale}`); - return res.json(await fetchProfiles(req.db, req.user.username, true)); + return res.json(await fetchProfiles(req.db, req.user.username, true, new ProfileOptions({}))); })); router.post('/profile/report/:username', handleErrorAsync(async (req, res) => {