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');
+    }
+  })
+);