add cors proxy and open graph fetching

This commit is contained in:
hzrd149 2023-06-17 10:04:22 -05:00
parent 4058f02d5a
commit 0cc405954f
20 changed files with 1907 additions and 11 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Fetch open graph metadata for links

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add CORS proxy

View File

@ -16,6 +16,7 @@
"@emotion/react": "^11.11.0", "@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"cheerio": "^1.0.0-rc.12",
"dayjs": "^1.11.8", "dayjs": "^1.11.8",
"framer-motion": "^7.10.3", "framer-motion": "^7.10.3",
"idb": "^7.1.1", "idb": "^7.1.1",

View File

@ -3,6 +3,7 @@ import appSettings from "../../services/app-settings";
import { ImageGalleryLink } from "../image-gallery"; import { ImageGalleryLink } from "../image-gallery";
import { useIsMobile } from "../../hooks/use-is-mobile"; import { useIsMobile } from "../../hooks/use-is-mobile";
import { useTrusted } from "../note/trust"; import { useTrusted } from "../note/trust";
import OpenGraphCard from "../open-graph-card";
const BlurredImage = (props: ImageProps) => { const BlurredImage = (props: ImageProps) => {
const { isOpen, onOpen } = useDisclosure(); const { isOpen, onOpen } = useDisclosure();
@ -44,9 +45,5 @@ export function renderVideoUrl(match: URL) {
} }
export function renderGenericUrl(match: URL) { export function renderGenericUrl(match: URL) {
return ( return <OpenGraphCard url={match} maxW="lg" />;
<Link color="blue.500" href={match.toString()} target="_blank" isExternal>
{match.toString()}
</Link>
);
} }

View File

@ -6,8 +6,6 @@ export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNo
return embedJSX(content, { return embedJSX(content, {
regexp: /:([a-zA-Z0-9]+):/i, regexp: /:([a-zA-Z0-9]+):/i,
render: (match) => { render: (match) => {
console.log(match);
const emojiTag = note.tags.find( const emojiTag = note.tags.find(
(tag) => tag[0] === "emoji" && tag[1].toLowerCase() === match[1].toLowerCase() && tag[2] (tag) => tag[0] === "emoji" && tag[1].toLowerCase() === match[1].toLowerCase() && tag[2]
); );

View File

@ -0,0 +1,32 @@
import { Box, CardProps, Code, Heading, Image, Link, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
import useOpenGraphData from "../hooks/use-open-graph-data";
export default function OpenGraphCard({ url, ...props }: { url: URL } & Omit<CardProps, "children">) {
const { value: data, loading } = useOpenGraphData(url);
const link = (
<Link href={url.toString()} isExternal color="blue.500">
{url.toString()}
</Link>
);
if (!data) return link;
return (
<LinkBox borderRadius="lg" borderWidth={1} overflow="hidden" {...props}>
{data.ogImage?.map((ogImage) => (
<Image src={ogImage.url} mx="auto" />
))}
<Box m="2" mt="4">
<Heading size="sm" my="2">
<LinkOverlay href={url.toString()} isExternal>
{data.ogTitle ?? data.dcTitle}
</LinkOverlay>
</Heading>
<Text>{data.ogDescription || data.dcDescription}</Text>
{link}
</Box>
</LinkBox>
);
}

17
src/helpers/cors.ts Normal file
View File

@ -0,0 +1,17 @@
import appSettings from "../services/app-settings";
import { convertToUrl } from "./url";
const corsFailedHosts = new Set();
export function fetchWithCorsFallback(url: URL | string, opts?: RequestInit) {
if (!appSettings.value.corsProxy) return fetch(url, opts);
if (corsFailedHosts.has(convertToUrl(url).host)) {
return fetch(appSettings.value.corsProxy + url, opts);
}
return fetch(url, opts).catch((e) => {
corsFailedHosts.add(convertToUrl(url).host);
return fetch(appSettings.value.corsProxy + url, opts);
});
}

View File

@ -1,4 +1,4 @@
const convertToUrl = (url: string | URL) => (url instanceof URL ? url : new URL(url)); export const convertToUrl = (url: string | URL) => (url instanceof URL ? url : new URL(url));
export function normalizeRelayUrl(relayUrl: string) { export function normalizeRelayUrl(relayUrl: string) {
const url = new URL(relayUrl); const url = new URL(relayUrl);

View File

@ -0,0 +1,13 @@
import { useAsync } from "react-use";
import extractMetaTags from "../lib/open-graph-scraper/extract";
import { fetchWithCorsFallback } from "../helpers/cors";
export default function useOpenGraphData(url: URL) {
return useAsync(async () => {
try {
const html = await fetchWithCorsFallback(url).then((res) => res.text());
return extractMetaTags(html);
} catch (e) {}
return null;
}, [url]);
}

View File

@ -0,0 +1 @@
open graph parser code copied from https://github.com/jshemas/openGraphScraper/blob/master/lib/extract.ts and modified to work in the browser

View File

@ -0,0 +1,54 @@
import { load } from "cheerio";
import fallback from "./fallback";
import fields from "./fields";
import mediaSetup from "./media";
import type { OgObjectInteral } from "./types";
/**
* extract all of the meta tags needed for ogs
*
* @param {sting} body - the body of the fetch request
* @param {object} options - options for ogs
* @return {object} object with ogs results
*
*/
export default function extractMetaTags(body: string, useFallbacks = true) {
let ogObject: OgObjectInteral = {};
const $ = load(body);
const metaFields = fields;
// find all of the open graph info in the meta tags
$("meta").each((index, meta) => {
if (!meta.attribs || (!meta.attribs.property && !meta.attribs.name)) return;
const property = meta.attribs.property || meta.attribs.name;
const content = meta.attribs.content || meta.attribs.value;
metaFields.forEach((item) => {
if (item && property.toLowerCase() === item.property.toLowerCase()) {
if (!item.multiple) {
// @ts-ignore
ogObject[item.fieldName] = content;
// @ts-ignore
} else if (!ogObject[item.fieldName]) {
// @ts-ignore
ogObject[item.fieldName] = [content];
// @ts-ignore
} else if (Array.isArray(ogObject[item.fieldName])) {
// @ts-ignore
ogObject[item.fieldName].push(content);
}
}
});
});
// formats the multiple media values
ogObject = mediaSetup(ogObject);
// if onlyGetOpenGraphInfo isn't set, run the open graph fallbacks
if (useFallbacks) {
ogObject = fallback(ogObject, $);
}
return ogObject;
}

View File

@ -0,0 +1,172 @@
import type { CheerioAPI } from "cheerio";
import { findImageTypeFromUrl, isImageTypeValid, isUrlValid } from "./utils";
import type { ImageObject, OgObjectInteral } from "./types";
const doesElementExist = (selector: string, attribute: string, $: CheerioAPI) =>
$(selector).attr(attribute) && ($(selector).attr(attribute)?.length || 0) > 0;
/**
* ogs fallbacks
*
* @param {object} ogObject - the current ogObject
* @param {object} $ - cheerio.load() of the current html
* @return {object} object with ogs results with updated fallback values
*
*/
export function fallback(ogObject: OgObjectInteral, $: CheerioAPI) {
// title fallback
if (!ogObject.ogTitle) {
if ($("title").text() && $("title").text().length > 0) {
ogObject.ogTitle = $("title").first().text();
} else if (
$('head > meta[name="title"]').attr("content") &&
($('head > meta[name="title"]').attr("content")?.length || 0) > 0
) {
ogObject.ogTitle = $('head > meta[name="title"]').attr("content");
} else if ($(".post-title").text() && $(".post-title").text().length > 0) {
ogObject.ogTitle = $(".post-title").text();
} else if ($(".entry-title").text() && $(".entry-title").text().length > 0) {
ogObject.ogTitle = $(".entry-title").text();
} else if ($('h1[class*="title" i] a').text() && $('h1[class*="title" i] a').text().length > 0) {
ogObject.ogTitle = $('h1[class*="title" i] a').text();
} else if ($('h1[class*="title" i]').text() && $('h1[class*="title" i]').text().length > 0) {
ogObject.ogTitle = $('h1[class*="title" i]').text();
}
}
// Get meta description tag if og description was not provided
if (!ogObject.ogDescription) {
if (doesElementExist('head > meta[name="description"]', "content", $)) {
ogObject.ogDescription = $('head > meta[name="description"]').attr("content");
} else if (doesElementExist('head > meta[itemprop="description"]', "content", $)) {
ogObject.ogDescription = $('head > meta[itemprop="description"]').attr("content");
} else if ($("#description").text() && $("#description").text().length > 0) {
ogObject.ogDescription = $("#description").text();
}
}
// Get all of images if there is no og:image info
if (!ogObject.ogImage) {
ogObject.ogImage = [];
$("img").map((index, imageElement) => {
const source: string = $(imageElement).attr("src") || "";
if (!source) return false;
const type = findImageTypeFromUrl(source);
if (!isUrlValid(source) || !isImageTypeValid(type)) return false;
const fallbackImage: ImageObject = {
url: source,
type,
};
if ($(imageElement).attr("width") && Number($(imageElement).attr("width")))
fallbackImage.width = Number($(imageElement).attr("width"));
if ($(imageElement).attr("height") && Number($(imageElement).attr("height")))
fallbackImage.height = Number($(imageElement).attr("height"));
ogObject.ogImage?.push(fallbackImage);
return false;
});
ogObject.ogImage = ogObject.ogImage
.filter((value) => value.url !== undefined && value.url !== "")
.filter((value, index) => index < 10);
if (ogObject.ogImage.length === 0) delete ogObject.ogImage;
} else if (ogObject.ogImage) {
ogObject.ogImage.map((image) => {
if (image.url && !image.type) {
const type = findImageTypeFromUrl(image.url);
if (isImageTypeValid(type)) image.type = type;
}
return false;
});
}
// audio fallback
if (!ogObject.ogAudioURL && !ogObject.ogAudioSecureURL) {
const audioElementValue: string = $("audio").attr("src") || "";
const audioSourceElementValue: string = $("audio > source").attr("src") || "";
if (doesElementExist("audio", "src", $)) {
if (audioElementValue.startsWith("https")) {
ogObject.ogAudioSecureURL = audioElementValue;
} else {
ogObject.ogAudioURL = audioElementValue;
}
const audioElementTypeValue: string = $("audio").attr("type") || "";
if (!ogObject.ogAudioType && doesElementExist("audio", "type", $)) ogObject.ogAudioType = audioElementTypeValue;
} else if (doesElementExist("audio > source", "src", $)) {
if (audioSourceElementValue.startsWith("https")) {
ogObject.ogAudioSecureURL = audioSourceElementValue;
} else {
ogObject.ogAudioURL = audioSourceElementValue;
}
const audioSourceElementTypeValue: string = $("audio > source").attr("type") || "";
if (!ogObject.ogAudioType && doesElementExist("audio > source", "type", $))
ogObject.ogAudioType = audioSourceElementTypeValue;
}
}
// locale fallback
if (!ogObject.ogLocale) {
if (doesElementExist("html", "lang", $)) {
ogObject.ogLocale = $("html").attr("lang");
} else if (doesElementExist('head > meta[itemprop="inLanguage"]', "content", $)) {
ogObject.ogLocale = $('head > meta[itemprop="inLanguage"]').attr("content");
}
}
// logo fallback
if (!ogObject.ogLogo) {
if (doesElementExist('meta[itemprop="logo"]', "content", $)) {
ogObject.ogLogo = $('meta[itemprop="logo"]').attr("content");
} else if (doesElementExist('img[itemprop="logo"]', "src", $)) {
ogObject.ogLogo = $('img[itemprop="logo"]').attr("src");
}
}
// url fallback
if (!ogObject.ogUrl) {
if (doesElementExist('link[rel="canonical"]', "href", $)) {
ogObject.ogUrl = $('link[rel="canonical"]').attr("href");
} else if (doesElementExist('link[rel="alternate"][hreflang="x-default"]', "href", $)) {
ogObject.ogUrl = $('link[rel="alternate"][hreflang="x-default"]').attr("href");
}
}
// date fallback
if (!ogObject.ogDate) {
if (doesElementExist('head > meta[name="date"]', "content", $)) {
ogObject.ogDate = $('head > meta[name="date"]').attr("content");
} else if (doesElementExist('[itemprop*="datemodified" i]', "content", $)) {
ogObject.ogDate = $('[itemprop*="datemodified" i]').attr("content");
} else if (doesElementExist('[itemprop="datepublished" i]', "content", $)) {
ogObject.ogDate = $('[itemprop="datepublished" i]').attr("content");
} else if (doesElementExist('[itemprop*="date" i]', "content", $)) {
ogObject.ogDate = $('[itemprop*="date" i]').attr("content");
} else if (doesElementExist('time[itemprop*="date" i]', "datetime", $)) {
ogObject.ogDate = $('time[itemprop*="date" i]').attr("datetime");
} else if (doesElementExist("time[datetime]", "datetime", $)) {
ogObject.ogDate = $("time[datetime]").attr("datetime");
}
}
// favicon fallback
if (!ogObject.favicon) {
if (doesElementExist('link[rel="shortcut icon"]', "href", $)) {
ogObject.favicon = $('link[rel="shortcut icon"]').attr("href");
} else if (doesElementExist('link[rel="icon"]', "href", $)) {
ogObject.favicon = $('link[rel="icon"]').attr("href");
} else if (doesElementExist('link[rel="mask-icon"]', "href", $)) {
ogObject.favicon = $('link[rel="mask-icon"]').attr("href");
} else if (doesElementExist('link[rel="apple-touch-icon"]', "href", $)) {
ogObject.favicon = $('link[rel="apple-touch-icon"]').attr("href");
} else if (doesElementExist('link[type="image/png"]', "href", $)) {
ogObject.favicon = $('link[type="image/png"]').attr("href");
} else if (doesElementExist('link[type="image/ico"]', "href", $)) {
ogObject.favicon = $('link[type="image/ico"]').attr("href");
} else if (doesElementExist('link[type="image/x-icon"]', "href", $)) {
ogObject.favicon = $('link[type="image/x-icon"]').attr("href");
}
}
return ogObject;
}
export default fallback;

View File

@ -0,0 +1,855 @@
/**
* array of meta tags ogs is looking for
*
* @return {array} array of meta tags
*
*/
const fields = [
{
multiple: false,
property: "og:title",
fieldName: "ogTitle",
},
{
multiple: false,
property: "og:type",
fieldName: "ogType",
},
{
multiple: false,
property: "og:logo",
fieldName: "ogLogo",
},
{
multiple: true,
property: "og:image",
fieldName: "ogImageProperty",
},
{
multiple: true,
property: "og:image:url",
fieldName: "ogImageURL",
},
{
multiple: true,
property: "og:image:secure_url",
fieldName: "ogImageSecureURL",
},
{
multiple: true,
property: "og:image:width",
fieldName: "ogImageWidth",
},
{
multiple: true,
property: "og:image:height",
fieldName: "ogImageHeight",
},
{
multiple: true,
property: "og:image:type",
fieldName: "ogImageType",
},
{
multiple: false,
property: "og:url",
fieldName: "ogUrl",
},
{
multiple: false,
property: "og:audio",
fieldName: "ogAudio",
},
{
multiple: false,
property: "og:audio:url",
fieldName: "ogAudioURL",
},
{
multiple: false,
property: "og:audio:secure_url",
fieldName: "ogAudioSecureURL",
},
{
multiple: false,
property: "og:audio:type",
fieldName: "ogAudioType",
},
{
multiple: false,
property: "og:description",
fieldName: "ogDescription",
},
{
multiple: false,
property: "og:determiner",
fieldName: "ogDeterminer",
},
{
multiple: false,
property: "og:locale",
fieldName: "ogLocale",
},
{
multiple: false,
property: "og:locale:alternate",
fieldName: "ogLocaleAlternate",
},
{
multiple: false,
property: "og:site_name",
fieldName: "ogSiteName",
},
{
multiple: false,
property: "og:product:retailer_item_id",
fieldName: "ogProductRetailerItemId",
},
{
multiple: false,
property: "og:product:price:amount",
fieldName: "ogProductPriceAmount",
},
{
multiple: false,
property: "og:product:price:currency",
fieldName: "ogProductPriceCurrency",
},
{
multiple: false,
property: "og:product:availability",
fieldName: "ogProductAvailability",
},
{
multiple: false,
property: "og:product:condition",
fieldName: "ogProductCondition",
},
{
multiple: false,
property: "og:price:amount",
fieldName: "ogPriceAmount",
},
{
multiple: false,
property: "og:price:currency",
fieldName: "ogPriceCurrency",
},
{
multiple: false,
property: "og:availability",
fieldName: "ogAvailability",
},
{
multiple: true,
property: "og:video",
fieldName: "ogVideoProperty",
},
{
multiple: true,
property: "og:video:url", // An alternative to 'og:video'
fieldName: "ogVideoProperty",
},
{
multiple: true,
property: "og:video:secure_url",
fieldName: "ogVideoSecureURL",
},
{
multiple: true,
property: "og:video:actor:id",
fieldName: "ogVideoActorId",
},
{
multiple: true,
property: "og:video:width",
fieldName: "ogVideoWidth",
},
{
multiple: true,
property: "og:video:height",
fieldName: "ogVideoHeight",
},
{
multiple: true,
property: "og:video:type",
fieldName: "ogVideoType",
},
{
multiple: false,
property: "twitter:card",
fieldName: "twitterCard",
},
{
multiple: false,
property: "twitter:url",
fieldName: "twitterUrl",
},
{
multiple: false,
property: "twitter:site",
fieldName: "twitterSite",
},
{
multiple: false,
property: "twitter:site:id",
fieldName: "twitterSiteId",
},
{
multiple: false,
property: "twitter:creator",
fieldName: "twitterCreator",
},
{
multiple: false,
property: "twitter:creator:id",
fieldName: "twitterCreatorId",
},
{
multiple: false,
property: "twitter:title",
fieldName: "twitterTitle",
},
{
multiple: false,
property: "twitter:description",
fieldName: "twitterDescription",
},
{
multiple: true,
property: "twitter:image",
fieldName: "twitterImageProperty",
},
{
multiple: true,
property: "twitter:image:height",
fieldName: "twitterImageHeight",
},
{
multiple: true,
property: "twitter:image:width",
fieldName: "twitterImageWidth",
},
{
multiple: true,
property: "twitter:image:src",
fieldName: "twitterImageSrc",
},
{
multiple: true,
property: "twitter:image:alt",
fieldName: "twitterImageAlt",
},
{
multiple: true,
property: "twitter:player",
fieldName: "twitterPlayerProperty",
},
{
multiple: true,
property: "twitter:player:width",
fieldName: "twitterPlayerWidth",
},
{
multiple: true,
property: "twitter:player:height",
fieldName: "twitterPlayerHeight",
},
{
multiple: true,
property: "twitter:player:stream",
fieldName: "twitterPlayerStream",
},
{
multiple: true,
property: "twitter:player:stream:content_type",
fieldName: "twitterPlayerStreamContentType",
},
{
multiple: false,
property: "twitter:app:name:iphone",
fieldName: "twitterAppNameiPhone",
},
{
multiple: false,
property: "twitter:app:id:iphone",
fieldName: "twitterAppIdiPhone",
},
{
multiple: false,
property: "twitter:app:url:iphone",
fieldName: "twitterAppUrliPhone",
},
{
multiple: false,
property: "twitter:app:name:ipad",
fieldName: "twitterAppNameiPad",
},
{
multiple: false,
property: "twitter:app:id:ipad",
fieldName: "twitterAppIdiPad",
},
{
multiple: false,
property: "twitter:app:url:ipad",
fieldName: "twitterAppUrliPad",
},
{
multiple: false,
property: "twitter:app:name:googleplay",
fieldName: "twitterAppNameGooglePlay",
},
{
multiple: false,
property: "twitter:app:id:googleplay",
fieldName: "twitterAppIdGooglePlay",
},
{
multiple: false,
property: "twitter:app:url:googleplay",
fieldName: "twitterAppUrlGooglePlay",
},
{
multiple: true,
property: "music:song",
fieldName: "musicSongProperty",
},
{
multiple: true,
property: "music:song:disc",
fieldName: "musicSongDisc",
},
{
multiple: true,
property: "music:song:track",
fieldName: "musicSongTrack",
},
{
multiple: true,
property: "music:song:url",
fieldName: "musicSongUrl",
},
{
multiple: true,
property: "music:musician",
fieldName: "musicMusician",
},
{
multiple: false,
property: "music:release_date",
fieldName: "musicReleaseDate",
},
{
multiple: false,
property: "music:duration",
fieldName: "musicDuration",
},
{
multiple: true,
property: "music:creator",
fieldName: "musicCreator",
},
{
multiple: true,
property: "music:album",
fieldName: "musicAlbum",
},
{
multiple: false,
property: "music:album:disc",
fieldName: "musicAlbumDisc",
},
{
multiple: false,
property: "music:album:track",
fieldName: "musicAlbumTrack",
},
{
multiple: false,
property: "music:album:url",
fieldName: "musicAlbumUrl",
},
{
multiple: false,
property: "article:published_date",
fieldName: "articlePublishedDate",
},
{
multiple: false,
property: "article:published_time",
fieldName: "articlePublishedTime",
},
{
multiple: false,
property: "article:modified_date",
fieldName: "articleModifiedDate",
},
{
multiple: false,
property: "article:modified_time",
fieldName: "articleModifiedTime",
},
{
multiple: false,
property: "article:expiration_time",
fieldName: "articleExpirationTime",
},
{
multiple: false,
property: "article:author",
fieldName: "articleAuthor",
},
{
multiple: false,
property: "article:section",
fieldName: "articleSection",
},
{
multiple: false,
property: "article:tag",
fieldName: "articleTag",
},
{
multiple: false,
property: "article:publisher",
fieldName: "articlePublisher",
},
{
multiple: false,
property: "og:article:published_time",
fieldName: "ogArticlePublishedTime",
},
{
multiple: false,
property: "og:article:modified_time",
fieldName: "ogArticleModifiedTime",
},
{
multiple: false,
property: "og:article:expiration_time",
fieldName: "ogArticleExpirationTime",
},
{
multiple: false,
property: "og:article:author",
fieldName: "ogArticleAuthor",
},
{
multiple: false,
property: "og:article:section",
fieldName: "ogArticleSection",
},
{
multiple: false,
property: "og:article:tag",
fieldName: "ogArticleTag",
},
{
multiple: false,
property: "og:article:publisher",
fieldName: "ogArticlePublisher",
},
{
multiple: false,
property: "books:book",
fieldName: "booksBook",
},
{
multiple: false,
property: "book:author",
fieldName: "bookAuthor",
},
{
multiple: false,
property: "book:isbn",
fieldName: "bookIsbn",
},
{
multiple: false,
property: "book:release_date",
fieldName: "bookReleaseDate",
},
{
multiple: false,
property: "book:canonical_name",
fieldName: "bookCanonicalName",
},
{
multiple: false,
property: "book:tag",
fieldName: "bookTag",
},
{
multiple: false,
property: "books:rating:value",
fieldName: "booksRatingValue",
},
{
multiple: false,
property: "books:rating:scale",
fieldName: "booksRatingScale",
},
{
multiple: false,
property: "profile:first_name",
fieldName: "profileFirstName",
},
{
multiple: false,
property: "profile:last_name",
fieldName: "profileLastName",
},
{
multiple: false,
property: "profile:username",
fieldName: "profileUsername",
},
{
multiple: false,
property: "profile:gender",
fieldName: "profileGender",
},
{
multiple: false,
property: "business:contact_data:street_address",
fieldName: "businessContactDataStreetAddress",
},
{
multiple: false,
property: "business:contact_data:locality",
fieldName: "businessContactDataLocality",
},
{
multiple: false,
property: "business:contact_data:region",
fieldName: "businessContactDataRegion",
},
{
multiple: false,
property: "business:contact_data:postal_code",
fieldName: "businessContactDataPostalCode",
},
{
multiple: false,
property: "business:contact_data:country_name",
fieldName: "businessContactDataCountryName",
},
{
multiple: false,
property: "restaurant:menu",
fieldName: "restaurantMenu",
},
{
multiple: false,
property: "restaurant:restaurant",
fieldName: "restaurantRestaurant",
},
{
multiple: false,
property: "restaurant:section",
fieldName: "restaurantSection",
},
{
multiple: false,
property: "restaurant:variation:price:amount",
fieldName: "restaurantVariationPriceAmount",
},
{
multiple: false,
property: "restaurant:variation:price:currency",
fieldName: "restaurantVariationPriceCurrency",
},
{
multiple: false,
property: "restaurant:contact_info:website",
fieldName: "restaurantContactInfoWebsite",
},
{
multiple: false,
property: "restaurant:contact_info:street_address",
fieldName: "restaurantContactInfoStreetAddress",
},
{
multiple: false,
property: "restaurant:contact_info:locality",
fieldName: "restaurantContactInfoLocality",
},
{
multiple: false,
property: "restaurant:contact_info:region",
fieldName: "restaurantContactInfoRegion",
},
{
multiple: false,
property: "restaurant:contact_info:postal_code",
fieldName: "restaurantContactInfoPostalCode",
},
{
multiple: false,
property: "restaurant:contact_info:country_name",
fieldName: "restaurantContactInfoCountryName",
},
{
multiple: false,
property: "restaurant:contact_info:email",
fieldName: "restaurantContactInfoEmail",
},
{
multiple: false,
property: "restaurant:contact_info:phone_number",
fieldName: "restaurantContactInfoPhoneNumber",
},
{
multiple: false,
property: "place:location:latitude",
fieldName: "placeLocationLatitude",
},
{
multiple: false,
property: "place:location:longitude",
fieldName: "placeLocationLongitude",
},
{
multiple: false,
property: "og:date",
fieldName: "ogDate",
},
{
multiple: false,
property: "author",
fieldName: "author",
},
{
multiple: false,
property: "updated_time",
fieldName: "updatedTime",
},
{
multiple: false,
property: "modified_time",
fieldName: "modifiedTime",
},
{
multiple: false,
property: "published_time",
fieldName: "publishedTime",
},
{
multiple: false,
property: "release_date",
fieldName: "releaseDate",
},
{
multiple: false,
property: "dc.source",
fieldName: "dcSource",
},
{
multiple: false,
property: "dc.subject",
fieldName: "dcSubject",
},
{
multiple: false,
property: "dc.title",
fieldName: "dcTitle",
},
{
multiple: false,
property: "dc.type",
fieldName: "dcType",
},
{
multiple: false,
property: "dc.creator",
fieldName: "dcCreator",
},
{
multiple: false,
property: "dc.coverage",
fieldName: "dcCoverage",
},
{
multiple: false,
property: "dc.language",
fieldName: "dcLanguage",
},
{
multiple: false,
property: "dc.contributor",
fieldName: "dcContributor",
},
{
multiple: false,
property: "dc.date",
fieldName: "dcDate",
},
{
multiple: false,
property: "dc.date.issued",
fieldName: "dcDateIssued",
},
{
multiple: false,
property: "dc.date.created",
fieldName: "dcDateCreated",
},
{
multiple: false,
property: "dc.description",
fieldName: "dcDescription",
},
{
multiple: false,
property: "dc.identifier",
fieldName: "dcIdentifier",
},
{
multiple: false,
property: "dc.publisher",
fieldName: "dcPublisher",
},
{
multiple: false,
property: "dc.rights",
fieldName: "dcRights",
},
{
multiple: false,
property: "dc.relation",
fieldName: "dcRelation",
},
{
multiple: false,
property: "dc.format.media",
fieldName: "dcFormatMedia",
},
{
multiple: false,
property: "dc.format.size",
fieldName: "dcFormatSize",
},
{
multiple: false,
property: "al:ios:url",
fieldName: "alIosUrl",
},
{
multiple: false,
property: "al:ios:app_store_id",
fieldName: "alIosAppStoreId",
},
{
multiple: false,
property: "al:ios:app_name",
fieldName: "alIosAppName",
},
{
multiple: false,
property: "al:iphone:url",
fieldName: "alIphoneUrl",
},
{
multiple: false,
property: "al:iphone:app_store_id",
fieldName: "alIphoneAppStoreId",
},
{
multiple: false,
property: "al:iphone:app_name",
fieldName: "alIphoneAppName",
},
{
multiple: false,
property: "al:ipad:url",
fieldName: "alIpadUrl",
},
{
multiple: false,
property: "al:ipad:app_store_id",
fieldName: "alIpadAppStoreId",
},
{
multiple: false,
property: "al:ipad:app_name",
fieldName: "alIpadAppName",
},
{
multiple: false,
property: "al:android:url",
fieldName: "alAndroidUrl",
},
{
multiple: false,
property: "al:android:package",
fieldName: "alAndroidPackage",
},
{
multiple: false,
property: "al:android:class",
fieldName: "alAndroidClass",
},
{
multiple: false,
property: "al:android:app_name",
fieldName: "alAndroidAppName",
},
{
multiple: false,
property: "al:windows_phone:url",
fieldName: "alWindowsPhoneUrl",
},
{
multiple: false,
property: "al:windows_phone:app_id",
fieldName: "alWindowsPhoneAppId",
},
{
multiple: false,
property: "al:windows_phone:app_name",
fieldName: "alWindowsPhoneAppName",
},
{
multiple: false,
property: "al:windows:url",
fieldName: "alWindowsUrl",
},
{
multiple: false,
property: "al:windows:app_id",
fieldName: "alWindowsAppId",
},
{
multiple: false,
property: "al:windows:app_name",
fieldName: "alWindowsAppName",
},
{
multiple: false,
property: "al:windows_universal:url",
fieldName: "alWindowsUniversalUrl",
},
{
multiple: false,
property: "al:windows_universal:app_id",
fieldName: "alWindowsUniversalAppId",
},
{
multiple: false,
property: "al:windows_universal:app_name",
fieldName: "alWindowsUniversalAppName",
},
{
multiple: false,
property: "al:web:url",
fieldName: "alWebUrl",
},
{
multiple: false,
property: "al:web:should_fallback",
fieldName: "alWebShouldFallback",
},
];
export default fields;

View File

@ -0,0 +1,234 @@
import fields from "./fields";
import { removeNestedUndefinedValues } from "./utils";
import type {
ImageObject,
MusicSongObject,
OgObjectInteral,
TwitterImageObject,
TwitterPlayerObject,
VideoObject,
} from "./types";
const mediaMapperTwitterImage = (item: TwitterImageObject[]) => ({
alt: item[3],
height: item[2],
url: item[0],
width: item[1],
});
const mediaMapperTwitterPlayer = (item: TwitterPlayerObject[]) => ({
height: item[2],
stream: item[3],
url: item[0],
width: item[1],
});
const mediaMapperMusicSong = (item: MusicSongObject[]) => ({
disc: item[2],
track: item[1],
url: item[0],
});
const mediaMapper = (item: ImageObject[] | VideoObject[]) => ({
height: item[2],
type: item[3],
url: item[0],
width: item[1],
});
const mediaSorter = (
a: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject,
b: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject
) => {
if (!(a.url && b.url)) {
return 0;
}
const aRes = a.url.match(/\.(\w{2,5})$/);
const aExt = (aRes && aRes[1].toLowerCase()) || null;
const bRes = b.url.match(/\.(\w{2,5})$/);
const bExt = (bRes && bRes[1].toLowerCase()) || null;
if (aExt === "gif" && bExt !== "gif") {
return -1;
}
if (aExt !== "gif" && bExt === "gif") {
return 1;
}
return Math.max(b.width || 0, b.height || 0) - Math.max(a.width || 0, a.height || 0);
};
const mediaSorterMusicSong = (a: MusicSongObject, b: MusicSongObject) => {
if (!(a.track && b.track)) {
return 0;
}
if ((a.disc || 0) > (b.disc || 0)) {
return 1;
}
if ((a.disc || 0) < (b.disc || 0)) {
return -1;
}
return a.track - b.track;
};
// lodash zip replacement
const zip = (array: any, ...args: any) => {
if (array === undefined) return [];
return array.map((value: any, idx: number) => [value, ...args.map((arr: []) => arr[idx])]);
};
/**
* formats the multiple media values
*
* @param {object} ogObject - the current ogObject
* @param {object} options - options for ogs
* @return {object} object with ogs results with updated media values
*
*/
export function mediaSetup(ogObject: OgObjectInteral) {
// sets ogImage property/width/height/type to empty array if one these exists
if (
ogObject.ogImageSecureURL ||
ogObject.ogImageURL ||
ogObject.ogImageProperty ||
ogObject.ogImageWidth ||
ogObject.ogImageHeight ||
ogObject.ogImageType
) {
ogObject.ogImageSecureURL = ogObject.ogImageSecureURL ? ogObject.ogImageSecureURL : [];
ogObject.ogImageURL = ogObject.ogImageURL ? ogObject.ogImageURL : [];
ogObject.ogImageProperty = ogObject.ogImageProperty ? ogObject.ogImageProperty : [];
// set ogImageProperty to ogImageSecureURL if it exists
// eslint-disable-next-line max-len
ogObject.ogImageProperty =
ogObject.ogImageSecureURL.length !== 0 ? ogObject.ogImageSecureURL : ogObject.ogImageProperty;
// fall back to ogImageURL if ogImageProperty isn't set
ogObject.ogImageProperty = ogObject.ogImageProperty.length !== 0 ? ogObject.ogImageProperty : ogObject.ogImageURL;
ogObject.ogImageWidth = ogObject.ogImageWidth ? ogObject.ogImageWidth : [];
ogObject.ogImageHeight = ogObject.ogImageHeight ? ogObject.ogImageHeight : [];
ogObject.ogImageType = ogObject.ogImageType ? ogObject.ogImageType : [];
}
// format images and limit to 10
const ogImages: ImageObject[] = zip(
ogObject.ogImageProperty,
ogObject.ogImageWidth,
ogObject.ogImageHeight,
ogObject.ogImageType
)
.map(mediaMapper)
.filter((value: ImageObject) => value.url !== undefined && value.url !== "")
.filter((value: ImageObject, index: number) => index < 10)
.sort(mediaSorter);
// sets ogVideo property/width/height/type to empty array if one these exists
if (ogObject.ogVideoProperty || ogObject.ogVideoWidth || ogObject.ogVideoHeight || ogObject.ogVideoType) {
ogObject.ogVideoProperty = ogObject.ogVideoProperty ? ogObject.ogVideoProperty : [];
ogObject.ogVideoWidth = ogObject.ogVideoWidth ? ogObject.ogVideoWidth : [];
ogObject.ogVideoHeight = ogObject.ogVideoHeight ? ogObject.ogVideoHeight : [];
ogObject.ogVideoType = ogObject.ogVideoType ? ogObject.ogVideoType : [];
}
// format videos and limit to 10
const ogVideos: VideoObject[] = zip(
ogObject.ogVideoProperty,
ogObject.ogVideoWidth,
ogObject.ogVideoHeight,
ogObject.ogVideoType
)
.map(mediaMapper)
.filter((value: VideoObject) => value.url !== undefined && value.url !== "")
.filter((value: VideoObject, index: number) => index < 10)
.sort(mediaSorter);
// sets twitter image src/property/width/height/alt to empty array if one these exists
if (
ogObject.twitterImageSrc ||
ogObject.twitterImageProperty ||
ogObject.twitterImageWidth ||
ogObject.twitterImageHeight ||
ogObject.twitterImageAlt
) {
ogObject.twitterImageSrc = ogObject.twitterImageSrc ? ogObject.twitterImageSrc : [];
// eslint-disable-next-line max-len
ogObject.twitterImageProperty = ogObject.twitterImageProperty
? ogObject.twitterImageProperty
: ogObject.twitterImageSrc; // deafult to twitterImageSrc
ogObject.twitterImageWidth = ogObject.twitterImageWidth ? ogObject.twitterImageWidth : [];
ogObject.twitterImageHeight = ogObject.twitterImageHeight ? ogObject.twitterImageHeight : [];
ogObject.twitterImageAlt = ogObject.twitterImageAlt ? ogObject.twitterImageAlt : [];
}
// format twitter images and limit to 10
const twitterImages: TwitterImageObject[] = zip(
ogObject.twitterImageProperty,
ogObject.twitterImageWidth,
ogObject.twitterImageHeight,
ogObject.twitterImageAlt
)
.map(mediaMapperTwitterImage)
.filter((value: TwitterImageObject) => value.url !== undefined && value.url !== "")
.filter((value: TwitterImageObject, index: number) => index < 10)
.sort(mediaSorter);
// sets twitter property/width/height/stream to empty array if one these exists
if (
ogObject.twitterPlayerProperty ||
ogObject.twitterPlayerWidth ||
ogObject.twitterPlayerHeight ||
ogObject.twitterPlayerStream
) {
ogObject.twitterPlayerProperty = ogObject.twitterPlayerProperty ? ogObject.twitterPlayerProperty : [];
ogObject.twitterPlayerWidth = ogObject.twitterPlayerWidth ? ogObject.twitterPlayerWidth : [];
ogObject.twitterPlayerHeight = ogObject.twitterPlayerHeight ? ogObject.twitterPlayerHeight : [];
ogObject.twitterPlayerStream = ogObject.twitterPlayerStream ? ogObject.twitterPlayerStream : [];
}
// format twitter player and limit to 10
const twitterPlayers: TwitterPlayerObject[] = zip(
ogObject.twitterPlayerProperty,
ogObject.twitterPlayerWidth,
ogObject.twitterPlayerHeight,
ogObject.twitterPlayerStream
)
.map(mediaMapperTwitterPlayer)
.filter((value: TwitterPlayerObject) => value.url !== undefined && value.url !== "")
.filter((value: TwitterPlayerObject, index: number) => index < 10)
.sort(mediaSorter);
// sets music property/songTrack/songDisc to empty array if one these exists
if (ogObject.musicSongProperty || ogObject.musicSongTrack || ogObject.musicSongDisc || ogObject.musicSongUrl) {
ogObject.musicSongUrl = ogObject.musicSongUrl ? ogObject.musicSongUrl : [];
ogObject.musicSongProperty = ogObject.musicSongProperty ? ogObject.musicSongProperty : ogObject.musicSongUrl; // deafult to musicSongUrl
ogObject.musicSongTrack = ogObject.musicSongTrack ? ogObject.musicSongTrack : [];
ogObject.musicSongDisc = ogObject.musicSongDisc ? ogObject.musicSongDisc : [];
}
// format music songs and limit to 10
const musicSongs: MusicSongObject[] = zip(ogObject.musicSongProperty, ogObject.musicSongTrack, ogObject.musicSongDisc)
.map(mediaMapperMusicSong)
.filter((value: MusicSongObject) => value.url !== undefined && value.url !== "")
.filter((value: MusicSongObject, index: number) => index < 10)
.sort(mediaSorterMusicSong);
// remove old values since everything will live under the main property
fields
.filter((item) => item.multiple && item.fieldName && item.fieldName.match("(ogImage|ogVideo|twitter|musicSong).*"))
.forEach((item) => {
// @ts-ignore
delete ogObject[item.fieldName];
});
if (ogImages.length) ogObject.ogImage = ogImages;
if (ogVideos.length) ogObject.ogVideo = ogVideos;
if (twitterImages.length) ogObject.twitterImage = twitterImages;
if (twitterPlayers.length) ogObject.twitterPlayer = twitterPlayers;
if (musicSongs.length) ogObject.musicSong = musicSongs;
// removes any undefs
ogObject = removeNestedUndefinedValues(ogObject);
return ogObject;
}
export default mediaSetup;

View File

@ -0,0 +1,240 @@
export type TwitterImageObject = {
alt?: string;
height?: number;
url: string;
width?: number;
};
export type TwitterPlayerObject = {
height?: number;
stream?: string;
url: string;
width?: number;
};
export type ImageObject = {
height?: number;
type: string;
url: string;
width?: number;
};
export type VideoObject = {
height?: number;
type?: string;
url: string;
width?: number;
};
export type MusicSongObject = {
disc?: string;
track?: number;
url: string;
};
export type OgObjectInteral = {
alAndroidAppName?: string;
alAndroidClass?: string;
alAndroidPackage?: string;
alAndroidUrl?: string;
alIosAppName?: string;
alIosAppStoreId?: string;
alIosUrl?: string;
alIpadAppName?: string;
alIpadAppStoreId?: string;
alIpadUrl?: string;
alIphoneAppName?: string;
alIphoneAppStoreId?: string;
alIphoneUrl?: string;
alWebShouldFallback?: string;
alWebUrl?: string;
alWindowsAppId?: string;
alWindowsAppName?: string;
alWindowsPhoneAppId?: string;
alWindowsPhoneAppName?: string;
alWindowsPhoneUrl?: string;
alWindowsUniversalAppId?: string;
alWindowsUniversalAppName?: string;
alWindowsUniversalUrl?: string;
alWindowsUrl?: string;
articleAuthor?: string;
articleExpirationTime?: string;
articleModifiedTime?: string;
articlePublishedTime?: string;
articlePublisher?: string;
articleSection?: string;
articleTag?: string;
author?: string;
bookAuthor?: string;
bookCanonicalName?: string;
bookIsbn?: string;
bookReleaseDate?: string;
booksBook?: string;
booksRatingScale?: string;
booksRatingValue?: string;
bookTag?: string;
businessContactDataCountryName?: string;
businessContactDataLocality?: string;
businessContactDataPostalCode?: string;
businessContactDataRegion?: string;
businessContactDataStreetAddress?: string;
charset?: string;
dcContributor?: string;
dcCoverage?: string;
dcCreator?: string;
dcDate?: string;
dcDateCreated?: string;
dcDateIssued?: string;
dcDescription?: string;
dcFormatMedia?: string;
dcFormatSize?: string;
dcIdentifier?: string;
dcLanguage?: string;
dcPublisher?: string;
dcRelation?: string;
dcRights?: string;
dcSource?: string;
dcSubject?: string;
dcTitle?: string;
dcType?: string;
error?: string;
errorDetails?: Error;
favicon?: string;
modifiedTime?: string;
musicAlbum?: string;
musicAlbumDisc?: string;
musicAlbumTrack?: string;
musicAlbumUrl?: string;
musicCreator?: string;
musicDuration?: string;
musicMusician?: string;
musicReleaseDate?: string;
musicSong?: MusicSongObject[];
musicSongDisc?: string | null[];
musicSongProperty?: string | null[];
musicSongTrack?: number | null[];
musicSongUrl?: string | null[];
ogArticleAuthor?: string;
ogArticleExpirationTime?: string;
ogArticleModifiedTime?: string;
ogArticlePublishedTime?: string;
ogArticlePublisher?: string;
ogArticleSection?: string;
ogArticleTag?: string;
ogAudio?: string;
ogAudioSecureURL?: string;
ogAudioType?: string;
ogAudioURL?: string;
ogAvailability?: string;
ogDate?: string;
ogDescription?: string;
ogDeterminer?: string;
ogImage?: ImageObject[];
ogImageHeight?: string | null[];
ogImageProperty?: string | null[];
ogImageSecureURL?: string | null[];
ogImageType?: string | null[];
ogImageURL?: string | null[];
ogImageWidth?: string | null[];
ogLocale?: string;
ogLocaleAlternate?: string;
ogLogo?: string;
ogPriceAmount?: string;
ogPriceCurrency?: string;
ogProductAvailability?: string;
ogProductCondition?: string;
ogProductPriceAmount?: string;
ogProductPriceCurrency?: string;
ogProductRetailerItemId?: string;
ogSiteName?: string;
ogTitle?: string;
ogType?: string;
ogUrl?: string;
ogVideo?: VideoObject[];
ogVideoActorId?: string;
ogVideoHeight?: string | null[];
ogVideoProperty?: string | null[];
ogVideoSecureURL?: string;
ogVideoType?: string | null[];
ogVideoWidth?: string | null[];
placeLocationLatitude?: string;
placeLocationLongitude?: string;
profileFirstName?: string;
profileGender?: string;
profileLastName?: string;
profileUsername?: string;
publishedTime?: string;
releaseDate?: string;
requestUrl?: string;
restaurantContactInfoCountryName?: string;
restaurantContactInfoEmail?: string;
restaurantContactInfoLocality?: string;
restaurantContactInfoPhoneNumber?: string;
restaurantContactInfoPostalCode?: string;
restaurantContactInfoRegion?: string;
restaurantContactInfoStreetAddress?: string;
restaurantContactInfoWebsite?: string;
restaurantMenu?: string;
restaurantRestaurant?: string;
restaurantSection?: string;
restaurantVariationPriceAmount?: string;
restaurantVariationPriceCurrency?: string;
success?: boolean;
twitterAppIdGooglePlay?: string;
twitterAppIdiPad?: string;
twitterAppIdiPhone?: string;
twitterAppNameGooglePlay?: string;
twitterAppNameiPad?: string;
twitterAppNameiPhone?: string;
twitterAppUrlGooglePlay?: string;
twitterAppUrliPad?: string;
twitterAppUrliPhone?: string;
twitterCard?: string;
twitterCreator?: string;
twitterCreatorId?: string;
twitterDescription?: string;
twitterImage?: TwitterImageObject[];
twitterImageAlt?: string | null[];
twitterImageHeight?: string | null[];
twitterImageProperty?: string | null[];
twitterImageSrc?: string | null[];
twitterImageWidth?: string | null[];
twitterPlayer?: TwitterPlayerObject[];
twitterPlayerHeight?: string | null[];
twitterPlayerProperty?: string | null[];
twitterPlayerStream?: string | null[];
twitterPlayerStreamContentType?: string;
twitterPlayerWidth?: string | null[];
twitterSite?: string;
twitterSiteId?: string;
twitterTitle?: string;
twitterUrl?: string;
updatedTime?: string;
};
export type OgObject = Omit<
OgObjectInteral,
| "musicSongDisc"
| "musicSongProperty"
| "musicSongTrack"
| "musicSongUrl"
| "ogImageHeight"
| "ogImageProperty"
| "ogImageSecureURL"
| "ogImageType"
| "ogImageURL"
| "ogImageWidth"
| "ogVideoHeight"
| "ogVideoProperty"
| "ogVideoType"
| "ogVideoWidth"
| "twitterImageAlt"
| "twitterImageHeight"
| "twitterImageProperty"
| "twitterImageSrc"
| "twitterImageWidth"
| "twitterPlayerHeight"
| "twitterPlayerProperty"
| "twitterPlayerStream"
| "twitterPlayerWidth"
>;

View File

@ -0,0 +1,139 @@
/**
* Checks if URL is valid
*
* @param {string} url - url to be checked
* @return {boolean} boolean value if the url is valid
*
*/
export function isUrlValid(url: string): boolean {
try {
new URL(url);
return true;
} catch (e) {}
return false;
}
/**
* Forces url to start with http:// if it doesn't
*
* @param {string} url - url to be updated
* @return {string} url that starts with http
*
*/
const coerceUrl = (url: string): string => (/^(f|ht)tps?:\/\//i.test(url) ? url : `http://${url}`);
/**
* Validates and formats url
*
* @param {string} url - url to be checked and formatted
* @return {string} proper url or null
*
*/
export function validateAndFormatURL(url: string): { url: string | null } {
return { url: isUrlValid(url) ? coerceUrl(url) : null };
}
/**
* Finds the image type from a given url
*
* @param {string} url - url to be checked
* @return {string} image type from url
*
*/
export function findImageTypeFromUrl(url: string): string {
let type: string = url.split(".").pop() || "";
[type] = type.split("?");
return type;
}
/**
* Checks if image type is valid
*
* @param {string} type - type to be checked
* @return {boolean} boolean value if type is value
*
*/
export function isImageTypeValid(type: string): boolean {
const validImageTypes: string[] = [
"apng",
"bmp",
"gif",
"ico",
"cur",
"jpg",
"jpeg",
"jfif",
"pjpeg",
"pjp",
"png",
"svg",
"tif",
"tiff",
"webp",
];
return validImageTypes.includes(type);
}
/**
* Checks if URL is a non html page
*
* @param {string} url - url to be checked
* @return {boolean} boolean value if url is non html
*
*/
export function isThisANonHTMLUrl(url: string): boolean {
const invalidImageTypes: string[] = [
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".3gp",
".avi",
".mov",
".mp4",
".m4v",
".m4a",
".mp3",
".mkv",
".ogv",
".ogm",
".ogg",
".oga",
".webm",
".wav",
".bmp",
".gif",
".jpg",
".jpeg",
".png",
".webp",
".zip",
".rar",
".tar",
".tar.gz",
".tgz",
".tar.bz2",
".tbz2",
".txt",
".pdf",
];
const extension: string = findImageTypeFromUrl(url);
return invalidImageTypes.some((type: string): boolean => `.${extension}`.includes(type));
}
/**
* Find and delete nested undefs
*
* @param {object} object - object to be cleaned
* @return {object} object without nested undefs
*
*/
export function removeNestedUndefinedValues(object: { [key: string]: any }): { [key: string]: any } {
Object.entries(object).forEach(([key, value]) => {
if (value && typeof value === "object") removeNestedUndefinedValues(value);
else if (value === undefined) delete object[key];
});
return object;
}

View File

@ -1,5 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import db from "./db"; import db from "./db";
import { fetchWithCorsFallback } from "../helpers/cors";
function parseAddress(address: string): { name?: string; domain?: string } { function parseAddress(address: string): { name?: string; domain?: string } {
const parts = address.trim().toLowerCase().split("@"); const parts = address.trim().toLowerCase().split("@");
@ -26,7 +27,9 @@ function getIdentityFromJson(name: string, domain: string, json: IdentityJson):
} }
async function fetchAllIdentities(domain: string) { async function fetchAllIdentities(domain: string) {
const json = await fetch(`//${domain}/.well-known/nostr.json`).then((res) => res.json() as Promise<IdentityJson>); const json = await fetchWithCorsFallback(`//${domain}/.well-known/nostr.json`).then(
(res) => res.json() as Promise<IdentityJson>
);
await addToCache(domain, json); await addToCache(domain, json);
} }
@ -35,7 +38,7 @@ async function fetchIdentity(address: string) {
const { name, domain } = parseAddress(address); const { name, domain } = parseAddress(address);
if (!name || !domain) throw new Error("invalid address"); if (!name || !domain) throw new Error("invalid address");
const json = await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`) const json = await fetchWithCorsFallback(`https://${domain}/.well-known/nostr.json?name=${name}`)
.then((res) => res.json() as Promise<IdentityJson>) .then((res) => res.json() as Promise<IdentityJson>)
.then((json) => { .then((json) => {
// convert all keys in names, and relays to lower case // convert all keys in names, and relays to lower case

View File

@ -26,6 +26,7 @@ export type AppSettings = {
zapAmounts: number[]; zapAmounts: number[];
primaryColor: string; primaryColor: string;
imageProxy: string; imageProxy: string;
corsProxy: string;
showContentWarning: boolean; showContentWarning: boolean;
twitterRedirect?: string; twitterRedirect?: string;
redditRedirect?: string; redditRedirect?: string;
@ -43,6 +44,7 @@ export const defaultSettings: AppSettings = {
zapAmounts: [50, 200, 500, 1000], zapAmounts: [50, 200, 500, 1000],
primaryColor: "#8DB600", primaryColor: "#8DB600",
imageProxy: "", imageProxy: "",
corsProxy: "",
showContentWarning: true, showContentWarning: true,
twitterRedirect: undefined, twitterRedirect: undefined,
redditRedirect: undefined, redditRedirect: undefined,

View File

@ -29,7 +29,7 @@ async function validateInvidiousUrl(url?: string) {
} }
export default function PrivacySettings() { export default function PrivacySettings() {
const { youtubeRedirect, twitterRedirect, redditRedirect, updateSettings } = useAppSettings(); const { youtubeRedirect, twitterRedirect, redditRedirect, corsProxy, updateSettings } = useAppSettings();
const { register, handleSubmit, formState } = useForm({ const { register, handleSubmit, formState } = useForm({
mode: "onBlur", mode: "onBlur",
@ -37,6 +37,7 @@ export default function PrivacySettings() {
youtubeRedirect, youtubeRedirect,
twitterRedirect, twitterRedirect,
redditRedirect, redditRedirect,
corsProxy,
}, },
}); });
@ -113,6 +114,19 @@ export default function PrivacySettings() {
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControl isInvalid={!!formState.errors.corsProxy}>
<FormLabel>CORS Proxy</FormLabel>
<Input type="url" placeholder="https://cors.example.com/" {...register("corsProxy")} />
{formState.errors.corsProxy && <FormErrorMessage>{formState.errors.corsProxy.message}</FormErrorMessage>}
<FormHelperText>
This is used as a fallback when verifying NIP-05 ids and fetching open-graph metadata. URL to an
instance of{" "}
<Link href="https://github.com/Rob--W/cors-anywhere" isExternal color="blue.500">
cors-anywhere
</Link>
</FormHelperText>
</FormControl>
<Button <Button
colorScheme="brand" colorScheme="brand"
ml="auto" ml="auto"

114
yarn.lock
View File

@ -3027,6 +3027,11 @@ bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -3158,6 +3163,31 @@ check-more-types@^2.24.0:
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==
cheerio-select@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==
dependencies:
boolbase "^1.0.0"
css-select "^5.1.0"
css-what "^6.1.0"
domelementtype "^2.3.0"
domhandler "^5.0.3"
domutils "^3.0.1"
cheerio@^1.0.0-rc.12:
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683"
integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==
dependencies:
cheerio-select "^2.1.0"
dom-serializer "^2.0.0"
domhandler "^5.0.3"
domutils "^3.0.1"
htmlparser2 "^8.0.1"
parse5 "^7.0.0"
parse5-htmlparser2-tree-adapter "^7.0.0"
ci-info@^3.1.0, ci-info@^3.2.0: ci-info@^3.1.0, ci-info@^3.2.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
@ -3353,6 +3383,17 @@ css-in-js-utils@^3.1.0:
dependencies: dependencies:
hyphenate-style-name "^1.0.3" hyphenate-style-name "^1.0.3"
css-select@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
dependencies:
boolbase "^1.0.0"
css-what "^6.1.0"
domhandler "^5.0.2"
domutils "^3.0.1"
nth-check "^2.0.1"
css-tree@^1.1.2: css-tree@^1.1.2:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
@ -3361,6 +3402,11 @@ css-tree@^1.1.2:
mdn-data "2.0.14" mdn-data "2.0.14"
source-map "^0.6.1" source-map "^0.6.1"
css-what@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
csstype@^3.0.11, csstype@^3.0.2, csstype@^3.0.6: csstype@^3.0.11, csstype@^3.0.2, csstype@^3.0.6:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
@ -3554,6 +3600,36 @@ dom-accessibility-api@^0.5.9:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
dom-serializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
entities "^4.2.0"
domelementtype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
domutils@^3.0.1:
version "3.1.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
dependencies:
dom-serializer "^2.0.0"
domelementtype "^2.3.0"
domhandler "^5.0.3"
ecc-jsbn@~0.1.1: ecc-jsbn@~0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@ -3593,6 +3669,11 @@ enquirer@^2.3.0, enquirer@^2.3.6:
dependencies: dependencies:
ansi-colors "^4.1.1" ansi-colors "^4.1.1"
entities@^4.2.0, entities@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
error-ex@^1.3.1: error-ex@^1.3.1:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -4209,6 +4290,16 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
htmlparser2@^8.0.1:
version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.3"
domutils "^3.0.1"
entities "^4.4.0"
http-signature@~1.3.6: http-signature@~1.3.6:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
@ -5001,6 +5092,13 @@ npm-run-path@^4.0.0:
dependencies: dependencies:
path-key "^3.0.0" path-key "^3.0.0"
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
dependencies:
boolbase "^1.0.0"
object-assign@^4.1.1: object-assign@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -5132,6 +5230,21 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0" json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6" lines-and-columns "^1.1.6"
parse5-htmlparser2-tree-adapter@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==
dependencies:
domhandler "^5.0.2"
parse5 "^7.0.0"
parse5@^7.0.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
dependencies:
entities "^4.4.0"
path-exists@^4.0.0: path-exists@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -6657,6 +6770,7 @@ wrap-ansi@^6.2.0:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0: wrap-ansi@^7.0.0:
name wrap-ansi-cjs
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==