diff options
author | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-10-01 14:44:32 +1000 |
---|---|---|
committer | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-10-01 14:44:32 +1000 |
commit | aef662521c1e6a607f6c4848f7c6af96da8900b6 (patch) | |
tree | 25d6256d4e8ae4496bc0f68c42a66fb20cbb8806 /src | |
parent | Make `afk` optional in ActivitySchema (diff) | |
download | server-aef662521c1e6a607f6c4848f7c6af96da8900b6.tar.xz |
Better embed handling
Diffstat (limited to 'src')
-rw-r--r-- | src/api/util/handlers/Message.ts | 143 | ||||
-rw-r--r-- | src/api/util/index.ts | 1 | ||||
-rw-r--r-- | src/api/util/utility/EmbedHandlers.ts | 140 |
3 files changed, 156 insertions, 128 deletions
diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index c0bdb6b0..09b86fc2 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -24,9 +24,8 @@ import { MessageCreateSchema, } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import fetch from "node-fetch"; -import cheerio from "cheerio"; import { In } from "typeorm"; +import { EmbedHandlers } from "@fosscord/api"; const allow_empty = false; // TODO: check webhook, application, system author, stickers // TODO: embed gifs/videos/images @@ -34,18 +33,6 @@ const allow_empty = false; const LINK_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; -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 async function handleMessage(opts: MessageOptions): Promise<Message> { const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, @@ -200,124 +187,24 @@ export async function postHandleMessage(message: Message) { links = links.slice(0, 20) as RegExpMatchArray; // embed max 20 links — TODO: make this configurable with instance policies - const { endpointPublic, resizeWidthMax, resizeHeightMax } = - Config.get().cdn; - for (const link of links) { - try { - const request = await fetch(link, { - ...DEFAULT_FETCH_OPTIONS, - size: Config.get().limits.message.maxEmbedDownloadSize, - }); - - let embed: Embed; - - const type = request.headers.get("content-type"); - if (type?.indexOf("image") == 0) { - embed = { - provider: { - url: link, - name: new URL(link).hostname, - }, - image: { - // can't be bothered rn - proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent( - link, - )}?width=500&height=400`, - url: link, - width: 500, - height: 400, - }, - }; - data.embeds.push(embed); - } else { - const text = await request.text(); - const $ = cheerio.load(text); - - const title = $('meta[property="og:title"]').attr("content"); - const provider_name = $('meta[property="og:site_name"]').text(); - const author_name = $('meta[property="article:author"]').attr( - "content", - ); - const description = - $('meta[property="og:description"]').attr("content") || - $('meta[property="description"]').attr("content"); + let embed: Embed; + const url = new URL(link); - const image = $('meta[property="og:image"]').attr("content"); - const width = - parseInt( - $('meta[property="og:image:width"]').attr("content") || - "", - ) || undefined; - const height = - parseInt( - $('meta[property="og:image:height"]').attr("content") || - "", - ) || undefined; + // bit gross, but whatever! + const { endpointPublic } = Config.get().cdn; + const handler = url.hostname == new URL(endpointPublic!).hostname ? EmbedHandlers["self"] : EmbedHandlers[url.hostname] || EmbedHandlers["default"]; - const url = $('meta[property="og:url"]').attr("content"); - // TODO: color - embed = { - provider: { - url: link, - name: provider_name, - }, - }; - - const resizeWidth = Math.min(resizeWidthMax ?? 1, width ?? 100); - const resizeHeight = Math.min( - resizeHeightMax ?? 1, - height ?? 100, - ); - if (author_name) embed.author = { name: author_name }; - if (image) - embed.thumbnail = { - proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent( - image, - )}?width=${resizeWidth}&height=${resizeHeight}`, - url: image, - width: width, - height: height, - }; - if (title) embed.title = title; - if (url) embed.url = url; - if (description) embed.description = description; - - const approvedProviders = [ - "media4.giphy.com", - "c.tenor.com", - // todo: make configurable? don't really care tho - ]; - - // very bad code below - // don't care lol - if ( - embed?.thumbnail?.url && - approvedProviders.indexOf( - new URL(embed.thumbnail.url).hostname, - ) !== -1 - ) { - embed = { - provider: { - url: link, - name: new URL(link).hostname, - }, - image: { - proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent( - image!, - )}?width=${resizeWidth}&height=${resizeHeight}`, - url: image, - width: width, - height: height, - }, - }; - } + try { + const res = await handler(url); + if (!res) continue; + embed = res; + } + catch (e) { + continue; + } - if (title || description) { - data.embeds.push(embed); - } - } - } catch (error) { } + data.embeds.push(embed); } await Promise.all([ diff --git a/src/api/util/index.ts b/src/api/util/index.ts index 9f375f72..ffad0607 100644 --- a/src/api/util/index.ts +++ b/src/api/util/index.ts @@ -7,3 +7,4 @@ export * from "./handlers/route"; export * from "./utility/String"; export * from "./handlers/Voice"; export * from "./utility/captcha"; +export * from "./utility/EmbedHandlers"; \ No newline at end of file diff --git a/src/api/util/utility/EmbedHandlers.ts b/src/api/util/utility/EmbedHandlers.ts new file mode 100644 index 00000000..d6b69d86 --- /dev/null +++ b/src/api/util/utility/EmbedHandlers.ts @@ -0,0 +1,140 @@ +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<Embed | null> => { + 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<Embed | null>; } = { + // 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<Embed | null> => { + 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<Embed | null> => { + 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, + } + }; + }, +}; \ No newline at end of file |