feat(api): Filtering profile data

This commit is contained in:
tecc 2023-06-01 21:02:51 +02:00
parent 5097e6c7a1
commit b1f9f39ca7
No known key found for this signature in database
GPG Key ID: 400AAD881FCC028B

View File

@ -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<string>} */
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) => {