From 39252c7828d210b571d53d15ceee1f9b7a8c7785 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Thu, 25 Jan 2024 16:16:49 +0100 Subject: [PATCH] allow clients to filter content api call --- website/server/controllers/api-v3/content.js | 69 +++++++++++++++++++- website/server/libs/content.js | 20 ++++-- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/website/server/controllers/api-v3/content.js b/website/server/controllers/api-v3/content.js index 76152c4e40..33bb64328e 100644 --- a/website/server/controllers/api-v3/content.js +++ b/website/server/controllers/api-v3/content.js @@ -1,4 +1,5 @@ import nconf from 'nconf'; +import fs from 'fs'; import { langCodes } from '../../libs/i18n'; import { CONTENT_CACHE_PATH, getLocalizedContentResponse } from '../../libs/content'; @@ -6,6 +7,28 @@ const IS_PROD = nconf.get('IS_PROD'); const api = {}; +const CACHED_HASHES = [ + +]; + +const MOBILE_FILTER = `achievements,questSeriesAchievements,animalColorAchievements,animalSetAchievements,stableAchievements, +mystery,bundles,loginIncentives,pets,premiumPets,specialPets,questPets,wackyPets,mounts,premiumMounts,specialMounts,questMounts, +events,dropEggs,questEggs,dropHatchingPotions,premiumHatchingPotions,wackyHatchingPotions,backgroundsFlat,questsByLevel,gear.tree, +tasksByCategory,userDefaults,timeTravelStable,gearTypes,cardTypes`; + +function hashForFilter (filter) { + let hash = 0; + let i; let + chr; + if (filter.length === 0) return ''; + for (i = 0; i < filter.length; i++) { // eslint-disable-line + chr = filter.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; // eslint-disable-line + hash |= 0; // eslint-disable-line + } + return String(hash); +} + /** * @api {get} /api/v3/content Get all available content objects * @apiDescription Does not require authentication. @@ -65,14 +88,54 @@ api.getContent = { language = proposedLang; } + let filter = req.query.filter || ''; + // apply defaults for mobile clients + if (filter === '') { + if (req.headers['x-client'] === 'habitica-android') { + filter = `${MOBILE_FILTER},appearance.background`; + } else if (req.headers['x-client'] === 'habitica-ios') { + filter = `${MOBILE_FILTER},backgrounds`; + } + } + + // Build usable filter object + const filterObj = {}; + filter.split(',').forEach(item => { + if (item.includes('.')) { + const [key, subkey] = item.split('.'); + if (!filterObj[key]) { + filterObj[key] = {}; + } + filterObj[key][subkey.trim()] = true; + } else { + filterObj[item.trim()] = true; + } + }); + if (IS_PROD) { - res.sendFile(`${CONTENT_CACHE_PATH}${language}.json`); + const filterHash = language + hashForFilter(filter); + if (CACHED_HASHES.includes(filterHash)) { + // Content is already cached, so just send it. + res.sendFile(`${CONTENT_CACHE_PATH}${filterHash}.json`); + } else { + // Content is not cached, so cache it and send it. + res.set({ + 'Content-Type': 'application/json', + }); + const jsonResString = getLocalizedContentResponse(language, filterObj); + fs.writeFileSync( + `${CONTENT_CACHE_PATH}${filterHash}.json`, + jsonResString, + 'utf8', + ); + CACHED_HASHES.push(filterHash); + res.status(200).send(jsonResString); + } } else { res.set({ 'Content-Type': 'application/json', }); - - const jsonResString = getLocalizedContentResponse(language); + const jsonResString = getLocalizedContentResponse(language, filterObj); res.status(200).send(jsonResString); } }, diff --git a/website/server/libs/content.js b/website/server/libs/content.js index 8328eb0ccb..378f7a09a6 100644 --- a/website/server/libs/content.js +++ b/website/server/libs/content.js @@ -5,23 +5,31 @@ import packageInfo from '../../../package.json'; export const CONTENT_CACHE_PATH = path.join(__dirname, '/../../../content_cache/'); -function walkContent (obj, lang) { +function walkContent (obj, lang, removedKeys = {}) { _.each(obj, (item, key, source) => { + if (key in removedKeys && removedKeys[key] === true) { + delete source[key]; + return; + } if (_.isPlainObject(item) || _.isArray(item)) { - walkContent(item, lang); + if (key in removedKeys && _.isPlainObject(removedKeys[key])) { + walkContent(item, lang, removedKeys[key]); + } else { + walkContent(item, lang); + } } else if (_.isFunction(item) && item.i18nLangFunc) { source[key] = item(lang); } }); } -export function localizeContentData (data, langCode) { +export function localizeContentData (data, langCode, removedKeys = {}) { const dataClone = _.cloneDeep(data); - walkContent(dataClone, langCode); + walkContent(dataClone, langCode, removedKeys); return dataClone; } -export function getLocalizedContentResponse (langCode) { - const localizedContent = localizeContentData(common.content, langCode); +export function getLocalizedContentResponse (langCode, removedKeys = {}) { + const localizedContent = localizeContentData(common.content, langCode, removedKeys); return `{"success": true, "data": ${JSON.stringify(localizedContent)}, "appVersion": "${packageInfo.version}"}`; }