Add link previews
Change-Id: I1792958844c18b4f4a65356dd5a07e3b2b39fcbc
diff --git a/server/src/routers/link-preview-router.ts b/server/src/routers/link-preview-router.ts
new file mode 100644
index 0000000..d7c5d42
--- /dev/null
+++ b/server/src/routers/link-preview-router.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Router } from 'express';
+import asyncHandler from 'express-async-handler';
+import { HttpStatusCode } from 'jami-web-common';
+import { getLinkPreview } from 'link-preview-js';
+import * as linkify from 'linkifyjs';
+
+import { authenticateToken } from '../middleware/auth.js';
+
+export const linkPreviewRouter = Router();
+
+// Result of getLinkPreview from link-preview-js
+type LinkPreviewJs = {
+ url: string;
+ title: string;
+ siteName: string | undefined;
+ description: string | undefined;
+ mediaType: string;
+ contentType: string | undefined;
+ images: string[];
+ videos: {
+ url: string | undefined;
+ secureUrl: string | null | undefined;
+ type: string | null | undefined;
+ width: string | undefined;
+ height: string | undefined;
+ }[];
+ favicons: string[];
+};
+
+linkPreviewRouter.use(authenticateToken);
+
+const linkPreviewOptions = {
+ // Allowing redirection from http to https
+ // Code from doc: https://github.com/ospfranco/link-preview-js#redirections
+ followRedirects: 'manual',
+ handleRedirects: (baseURL: string, forwardedURL: string) => {
+ const urlObj = new URL(baseURL);
+ const forwardedURLObj = new URL(forwardedURL);
+ if (
+ forwardedURLObj.hostname === urlObj.hostname ||
+ forwardedURLObj.hostname === 'www.' + urlObj.hostname ||
+ 'www.' + forwardedURLObj.hostname === urlObj.hostname
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+} as const;
+
+linkPreviewRouter.get(
+ '/',
+ asyncHandler(async (req, res) => {
+ const url = req.query.url;
+
+ if (typeof url !== 'string') {
+ res.status(HttpStatusCode.BadRequest).send('Invalid query parameters');
+ return;
+ }
+
+ // Add 'http' or 'https' if absent. This is required by getLinkPreview
+ const sanitizedUrl = linkify.find(url)[0]?.href;
+
+ if (!sanitizedUrl) {
+ res.status(HttpStatusCode.BadRequest).send('Invalid url');
+ return;
+ }
+
+ try {
+ const detailedLinkPreview = (await getLinkPreview(sanitizedUrl, linkPreviewOptions)) as LinkPreviewJs;
+ const linkPreview = {
+ title: detailedLinkPreview.title,
+ description: detailedLinkPreview.description,
+ // We might eventualy want to compare the images in order to select the best fit
+ // https://andrejgajdos.com/how-to-create-a-link-preview/
+ image: detailedLinkPreview.images[0] ?? detailedLinkPreview.favicons[0],
+ };
+ res.json(linkPreview).end();
+ } catch (e) {
+ res.status(HttpStatusCode.NotFound).send('Could not access url');
+ }
+ })
+);