diff --git a/.env.sample b/.env.sample index 3194f024c..c0f5c18e5 100644 --- a/.env.sample +++ b/.env.sample @@ -181,3 +181,7 @@ RATE_LIMITER_ENABLED=true # Configure default throttling parameters for rate limiter RATE_LIMITER_REQUESTS=1000 RATE_LIMITER_DURATION_WINDOW=60 + +# Iframely API config +IFRAMELY_URL= +IFRAMELY_API_KEY= diff --git a/plugins/iframely/plugin.json b/plugins/iframely/plugin.json new file mode 100644 index 000000000..e4a500e96 --- /dev/null +++ b/plugins/iframely/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "Iframely", + "description": "Integrate Iframely to enable unfurling of arbitrary urls", + "requiredEnvVars": ["IFRAMELY_URL", "IFRAMELY_API_KEY"] +} diff --git a/plugins/iframely/server/.babelrc b/plugins/iframely/server/.babelrc new file mode 100644 index 000000000..c87001bc4 --- /dev/null +++ b/plugins/iframely/server/.babelrc @@ -0,0 +1,3 @@ +{ + "extends": "../../../server/.babelrc" +} diff --git a/plugins/iframely/server/iframely.ts b/plugins/iframely/server/iframely.ts new file mode 100644 index 000000000..3a359f1c5 --- /dev/null +++ b/plugins/iframely/server/iframely.ts @@ -0,0 +1,24 @@ +import fetch from "fetch-with-proxy"; +import env from "@server/env"; +import { InvalidRequestError } from "@server/errors"; + +class Iframely { + private static apiUrl = `${env.IFRAMELY_URL}/api`; + private static apiKey = env.IFRAMELY_API_KEY; + + public static async get(url: string, type = "oembed") { + try { + const res = await fetch( + `${this.apiUrl}/${type}?url=${encodeURIComponent(url)}&api_key=${ + this.apiKey + }` + ); + const data = await res.json(); + return data; + } catch (err) { + throw InvalidRequestError(err); + } + } +} + +export default Iframely; diff --git a/plugins/iframely/server/unfurl.ts b/plugins/iframely/server/unfurl.ts new file mode 100644 index 000000000..a70e2212d --- /dev/null +++ b/plugins/iframely/server/unfurl.ts @@ -0,0 +1,3 @@ +import Iframely from "./iframely"; + +export const unfurl = async (url: string) => Iframely.get(url); diff --git a/server/env.ts b/server/env.ts index 9bb1a114a..47c71eb09 100644 --- a/server/env.ts +++ b/server/env.ts @@ -601,6 +601,24 @@ export class Environment { this.AWS_S3_UPLOAD_MAX_SIZE ); + /** + * Iframely url + */ + @IsOptional() + @IsUrl({ + require_tld: false, + allow_underscores: true, + protocols: ["http", "https"], + }) + public IFRAMELY_URL = process.env.IFRAMELY_URL ?? "https://iframe.ly"; + + /** + * Iframely API key + */ + @IsOptional() + @CannotUseWithout("IFRAMELY_URL") + public IFRAMELY_API_KEY = this.toOptionalString(process.env.IFRAMELY_API_KEY); + /** * The product name */ diff --git a/server/routes/api/urls/urls.ts b/server/routes/api/urls/urls.ts index 29c5db17b..634fb1e5e 100644 --- a/server/routes/api/urls/urls.ts +++ b/server/routes/api/urls/urls.ts @@ -10,6 +10,7 @@ import { authorize } from "@server/policies"; import { presentDocument, presentMention } from "@server/presenters/unfurls"; import { APIContext } from "@server/types"; import { RateLimiterStrategy } from "@server/utils/RateLimiter"; +import { Iframely } from "@server/utils/unfurl"; import * as T from "./schema"; const router = new Router(); @@ -47,22 +48,21 @@ router.post( } const previewDocumentId = parseDocumentSlug(url); - if (!previewDocumentId) { - ctx.response.status = 204; + if (previewDocumentId) { + const document = previewDocumentId + ? await Document.findByPk(previewDocumentId) + : undefined; + if (!document) { + throw NotFoundError("Document does not exist"); + } + authorize(actor, "read", document); + + ctx.body = presentDocument(document, actor); return; } - const document = previewDocumentId - ? await Document.findByPk(previewDocumentId, { - userId: actor.id, - }) - : undefined; - if (!document) { - throw NotFoundError("Document does not exist"); - } - authorize(actor, "read", document); - - ctx.body = presentDocument(document, actor); + const data = await Iframely.unfurl(url); + ctx.body = data; } ); diff --git a/server/types.ts b/server/types.ts index d6c6670d9..6155d7d8c 100644 --- a/server/types.ts +++ b/server/types.ts @@ -446,3 +446,7 @@ export type CollectionJSONExport = { [id: string]: AttachmentJSONExport; }; }; + +export type UnfurlResolver = { + unfurl: (url: string) => Promise; +}; diff --git a/server/utils/unfurl.ts b/server/utils/unfurl.ts new file mode 100644 index 000000000..b9518fd5b --- /dev/null +++ b/server/utils/unfurl.ts @@ -0,0 +1,24 @@ +import path from "path"; +import glob from "glob"; +import { startCase } from "lodash"; +import env from "@server/env"; +import Logger from "@server/logging/Logger"; +import { UnfurlResolver } from "@server/types"; + +const resolvers: Record = {}; +const rootDir = env.ENVIRONMENT === "test" ? "" : "build"; + +glob + .sync(path.join(rootDir, "plugins/*/server/unfurl.js")) + .forEach((filePath: string) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const resolver: UnfurlResolver = require(path.join( + process.cwd(), + filePath + )); + const name = startCase(filePath.split("/")[2]); + resolvers[name] = resolver; + Logger.debug("utils", `Registered unfurl resolver ${filePath}`); + }); + +export const Iframely = resolvers["Iframely"];