diff --git a/plugins/iframely/server/iframely.ts b/plugins/iframely/server/iframely.ts index 25403e57c..bcba36122 100644 --- a/plugins/iframely/server/iframely.ts +++ b/plugins/iframely/server/iframely.ts @@ -1,57 +1,22 @@ import type { Unfurl } from "@shared/types"; -import { Day } from "@shared/utils/time"; import { InternalError } from "@server/errors"; -import Logger from "@server/logging/Logger"; -import Redis from "@server/storage/redis"; import fetch from "@server/utils/fetch"; import env from "./env"; class Iframely { private static apiUrl = `${env.IFRAMELY_URL}/api`; private static apiKey = env.IFRAMELY_API_KEY; - private static cacheKeyPrefix = "unfurl"; - private static defaultCacheExpiry = Day; - - private static cacheKey(url: string) { - return `${this.cacheKeyPrefix}-${url}`; - } - - private static async cache(url: string, response: any) { - // do not cache error responses - if (response.error) { - return; - } - try { - await Redis.defaultClient.set( - this.cacheKey(url), - JSON.stringify(response), - "EX", - response.cache_age || this.defaultCacheExpiry - ); - } catch (err) { - // just log it, can skip caching and directly return response - Logger.error("Could not cache Iframely response", err); - } - } public static async fetch(url: string, type = "oembed") { - const res = await fetch( - `${this.apiUrl}/${type}?url=${encodeURIComponent(url)}&api_key=${ - this.apiKey - }` - ); - return res.json(); - } - - private static async cached(url: string) { try { - const val = await Redis.defaultClient.get(this.cacheKey(url)); - if (val) { - return JSON.parse(val); - } + const res = await fetch( + `${this.apiUrl}/${type}?url=${encodeURIComponent(url)}&api_key=${ + this.apiKey + }` + ); + return res.json(); } catch (err) { - // just log it, response can still be obtained using the fetch call - Logger.error("Could not fetch cached Iframely response", err); + throw InternalError(err); } } @@ -61,18 +26,8 @@ class Iframely { * @param url * @returns Preview data for the url */ - public static async get(url: string): Promise { - try { - const cached = await Iframely.cached(url); - if (cached) { - return cached; - } - const res = await Iframely.fetch(url); - await Iframely.cache(url, res); - return res; - } catch (err) { - throw InternalError(err); - } + public static async unfurl(url: string): Promise { + return Iframely.fetch(url); } } diff --git a/plugins/iframely/server/index.ts b/plugins/iframely/server/index.ts index cf05aece3..15f5a0b3d 100644 --- a/plugins/iframely/server/index.ts +++ b/plugins/iframely/server/index.ts @@ -1,3 +1,4 @@ +import { Day } from "@shared/utils/time"; import { PluginManager, PluginPriority, @@ -12,7 +13,7 @@ if (enabled) { PluginManager.add([ { type: Hook.UnfurlProvider, - value: Iframely.get, + value: { unfurl: Iframely.unfurl, cacheExpiry: Day }, // Make sure this is last in the stack to be evaluated after all other unfurl providers priority: PluginPriority.VeryLow, diff --git a/server/routes/api/urls/urls.ts b/server/routes/api/urls/urls.ts index 6790b5a0d..a80abd671 100644 --- a/server/routes/api/urls/urls.ts +++ b/server/routes/api/urls/urls.ts @@ -13,6 +13,7 @@ import { authorize } from "@server/policies"; import { presentDocument, presentMention } from "@server/presenters/unfurls"; import presentUnfurl from "@server/presenters/unfurls/unfurl"; import { APIContext } from "@server/types"; +import { CacheHelper } from "@server/utils/CacheHelper"; import { Hook, PluginManager } from "@server/utils/PluginManager"; import { RateLimiterStrategy } from "@server/utils/RateLimiter"; import * as T from "./schema"; @@ -75,12 +76,26 @@ router.post( } // External resources + const cachedData = await CacheHelper.getData( + CacheHelper.getUnfurlKey(url, actor.teamId) + ); + if (cachedData) { + return (ctx.body = presentUnfurl(cachedData)); + } + for (const plugin of plugins) { - const data = await plugin.value(url); + const data = await plugin.value.unfurl(url); if (data) { - return "error" in data - ? (ctx.response.status = 204) - : (ctx.body = presentUnfurl(data)); + if ("error" in data) { + return (ctx.response.status = 204); + } else { + await CacheHelper.setData( + CacheHelper.getUnfurlKey(url, actor.teamId), + data, + plugin.value.cacheExpiry + ); + return (ctx.body = presentUnfurl(data)); + } } } diff --git a/server/utils/CacheHelper.ts b/server/utils/CacheHelper.ts new file mode 100644 index 000000000..de5a49e39 --- /dev/null +++ b/server/utils/CacheHelper.ts @@ -0,0 +1,66 @@ +import { Day } from "@shared/utils/time"; +import Logger from "@server/logging/Logger"; +import Redis from "@server/storage/redis"; + +/** + * A Helper class for server-side cache management + */ +export class CacheHelper { + private static defaultDataExpiry = Day; + + /** + * Given a key, gets the data from cache store + * + * @param key Key against which data will be accessed + */ + public static async getData(key: string) { + try { + const data = await Redis.defaultClient.get(key); + if (data) { + return JSON.parse(data); + } + } catch (err) { + // just log it, response can still be obtained using the fetch call + Logger.error(`Could not fetch cached response against ${key}`, err); + } + } + + /** + * Given a key, data and cache config, saves the data in cache store + * + * @param key Cache key + * @param data Data to be saved against the key + * @param expiry Cache data expiry + */ + public static async setData( + key: string, + data: Record, + expiry?: number + ) { + if ("error" in data) { + return; + } + + try { + await Redis.defaultClient.set( + key, + JSON.stringify(data), + "EX", + expiry || this.defaultDataExpiry + ); + } catch (err) { + // just log it, can skip caching and directly return response + Logger.error(`Could not cache response against ${key}`, err); + } + } + + /** + * Gets key against which unfurl response for the given url is stored + * + * @param url The url to generate a key for + * @param teamId The team ID to generate a key for + */ + public static getUnfurlKey(url: string, teamId: string) { + return `unfurl:${teamId}:${url}`; + } +} diff --git a/server/utils/PluginManager.ts b/server/utils/PluginManager.ts index 97939c794..e203e1bed 100644 --- a/server/utils/PluginManager.ts +++ b/server/utils/PluginManager.ts @@ -40,7 +40,7 @@ type PluginValueMap = { [Hook.EmailTemplate]: typeof BaseEmail; [Hook.Processor]: typeof BaseProcessor; [Hook.Task]: typeof BaseTask; - [Hook.UnfurlProvider]: UnfurlSignature; + [Hook.UnfurlProvider]: { unfurl: UnfurlSignature; cacheExpiry: number }; }; export type Plugin = {