/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
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 .
*/
import { Config } from "@spacebar/util";
import { Embed, EmbedImage, EmbedType } from "@spacebar/schemas";
import * as cheerio from "cheerio";
import crypto from "crypto";
import { yellow } from "picocolors";
import probe from "probe-image-size";
export const DEFAULT_FETCH_OPTIONS: RequestInit = {
redirect: "follow",
headers: {
"user-agent": "Mozilla/5.0 (compatible; Spacebar/1.0; +https://github.com/spacebarchat/server)",
"accept-language": "en-US,en;q=0.9",
},
// size: 1024 * 1024 * 5, // grabbed from config later
method: "GET",
};
const makeEmbedImage = (url: string | undefined, width: number | undefined, height: number | undefined): Required | undefined => {
if (!url || !width || !height) return undefined;
return {
url,
width,
height,
proxy_url: getProxyUrl(new URL(url), width, height),
};
};
let hasWarnedAboutImagor = false;
export const getProxyUrl = (url: URL, width: number, height: number): string => {
const { resizeWidthMax, resizeHeightMax, imagorServerUrl } = Config.get().cdn;
const secret = Config.get().security.requestSignature;
width = Math.min(width || 500, resizeWidthMax || width);
height = Math.min(height || 500, resizeHeightMax || width);
// Imagor
if (imagorServerUrl) {
const path = `${width}x${height}/${url.host}${url.pathname}`;
const hash = crypto.createHmac("sha1", secret).update(path).digest("base64").replace(/\+/g, "-").replace(/\//g, "_");
return `${imagorServerUrl}/${hash}/${path}`;
}
if (!hasWarnedAboutImagor) {
hasWarnedAboutImagor = true;
console.log("[Embeds]", yellow("Imagor has not been set up correctly. https://docs.spacebar.chat/setup/server/configuration/imagor/"));
}
return url.toString();
};
const getMeta = ($: cheerio.CheerioAPI, name: string): string | undefined => {
let elem = $(`meta[property="${name}"]`);
if (!elem.length) elem = $(`meta[name="${name}"]`);
const ret = elem.attr("content") || elem.text();
return ret.trim().length == 0 ? undefined : ret;
};
const tryParseInt = (str: string | undefined) => {
if (!str) return undefined;
try {
return parseInt(str);
} catch (e) {
return undefined;
}
};
export const getMetaDescriptions = (text: string) => {
const $ = cheerio.load(text);
return {
type: getMeta($, "og:type"),
title: getMeta($, "og:title") || $("title").first().text(),
provider_name: getMeta($, "og:site_name"),
author: getMeta($, "article:author"),
description: getMeta($, "og:description") || getMeta($, "description"),
image: getMeta($, "og:image") || getMeta($, "twitter:image"),
image_fallback: $(`image`).attr("src"),
video_fallback: $(`video`).attr("src"),
width: tryParseInt(getMeta($, "og:image:width")),
height: tryParseInt(getMeta($, "og:image:height")),
url: getMeta($, "og:url"),
youtube_embed: getMeta($, "og:video:secure_url"),
site_name: getMeta($, "og:site_name"),
$,
};
};
const doFetch = async (url: URL, opts?: RequestInit) => {
try {
const res = await fetch(url, {
...DEFAULT_FETCH_OPTIONS,
...opts,
});
if (res.headers.get("content-length")) {
const contentLength = parseInt(res.headers.get("content-length")!);
if (Config.get().limits.message.maxEmbedDownloadSize && contentLength > Config.get().limits.message.maxEmbedDownloadSize) {
return null;
}
}
return res;
} catch (e) {
return null;
}
};
const genericImageHandler = async (url: URL): Promise