import { Config, Embed, EmbedType } from "@fosscord/util"; import fetch, { Response } from "node-fetch"; import * as cheerio from "cheerio"; import probe from "probe-image-size"; export const DEFAULT_FETCH_OPTIONS: any = { redirect: "follow", follow: 1, headers: { "user-agent": "Mozilla/5.0 (compatible; Fosscord/1.0; +https://github.com/fosscord/fosscord)", }, // size: 1024 * 1024 * 5, // grabbed from config later compress: true, method: "GET", }; export const getProxyUrl = (url: URL, width: number, height: number) => { const { endpointPublic, resizeWidthMax, resizeHeightMax } = Config.get().cdn; width = Math.min(width || 500, resizeWidthMax || width); height = Math.min(height || 500, resizeHeightMax || width); return `${endpointPublic}/external/resize/${encodeURIComponent(url.href)}?width=${width}&height=${height}`; }; export const getMetaDescriptions = async (url: URL) => { let response: Response; try { response = await fetch(url, { ...DEFAULT_FETCH_OPTIONS, size: Config.get().limits.message.maxEmbedDownloadSize, }); } catch (e) { return null; } const text = await response.text(); const $ = cheerio.load(text); return { title: $('meta[property="og:title"]').attr("content"), provider_name: $('meta[property="og:site_name"]').text(), author: $('meta[property="article:author"]').attr("content"), description: $('meta[property="og:description"]').attr("content") || $('meta[property="description"]').attr("content"), image: $('meta[property="og:image"]').attr("content"), width: parseInt( $('meta[property="og:image:width"]').attr("content") || "", ) || undefined, height: parseInt( $('meta[property="og:image:height"]').attr("content") || "", ) || undefined, url: $('meta[property="og:url"]').attr("content"), youtube_embed: $(`meta[property="og:video:secure_url"]`).attr("content") }; }; const genericImageHandler = async (url: URL): Promise => { const metas = await getMetaDescriptions(url); if (!metas) return null; const result = await probe(url.href); const width = metas.width || result.width; const height = metas.height || result.height; return { url: url.href, type: EmbedType.image, thumbnail: { width: width, height: height, url: url.href, proxy_url: getProxyUrl(url, result.width, result.height), } }; }; export const EmbedHandlers: { [key: string]: (url: URL) => Promise; } = { // the url does not have a special handler "default": genericImageHandler, "giphy.com": genericImageHandler, "media4.giphy.com": genericImageHandler, "tenor.com": genericImageHandler, "c.tenor.com": genericImageHandler, "media.tenor.com": genericImageHandler, "www.youtube.com": async (url: URL): Promise => { const metas = await getMetaDescriptions(url); if (!metas) return null; return { video: { // TODO: does this adjust with aspect ratio? width: metas.width, height: metas.height, url: metas.youtube_embed!, }, url: url.href, type: EmbedType.video, title: metas.title, thumbnail: { width: metas.width, height: metas.height, url: metas.image, proxy_url: getProxyUrl(new URL(metas.image!), metas.width!, metas.height!), }, provider: { url: "https://www.youtube.com", name: "YouTube", }, description: metas.description, color: 16711680, author: { name: metas.author, // TODO: author channel url } }; }, // the url is an image from this instance "self": async (url: URL): Promise => { const result = await probe(url.href); return { url: url.href, type: EmbedType.image, thumbnail: { width: result.width, height: result.height, url: url.href, proxy_url: url.href, } }; }, };