Fetch favicon from NIP-05 domain

This commit is contained in:
Alex Gleason 2024-08-17 09:21:12 -05:00
parent 79ac4ada81
commit e539a29775
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
4 changed files with 101 additions and 1 deletions

View file

@ -24,6 +24,7 @@
"exclude": ["./public"], "exclude": ["./public"],
"imports": { "imports": {
"@/": "./src/", "@/": "./src/",
"@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47",
"@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
"@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@db/sqlite": "jsr:@db/sqlite@^0.11.1",
"@hono/hono": "jsr:@hono/hono@^4.4.6", "@hono/hono": "jsr:@hono/hono@^4.4.6",

46
deno.lock generated
View file

@ -2,9 +2,11 @@
"version": "3", "version": "3",
"packages": { "packages": {
"specifiers": { "specifiers": {
"jsr:@b-fuze/deno-dom@^0.1.47": "jsr:@b-fuze/deno-dom@0.1.47",
"jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.6", "jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.6",
"jsr:@db/sqlite@^0.11.1": "jsr:@db/sqlite@0.11.1", "jsr:@db/sqlite@^0.11.1": "jsr:@db/sqlite@0.11.1",
"jsr:@denosaurs/plug@1": "jsr:@denosaurs/plug@1.0.6", "jsr:@denosaurs/plug@1": "jsr:@denosaurs/plug@1.0.6",
"jsr:@denosaurs/plug@1.0.3": "jsr:@denosaurs/plug@1.0.3",
"jsr:@gleasonator/policy": "jsr:@gleasonator/policy@0.2.0", "jsr:@gleasonator/policy": "jsr:@gleasonator/policy@0.2.0",
"jsr:@gleasonator/policy@0.2.0": "jsr:@gleasonator/policy@0.2.0", "jsr:@gleasonator/policy@0.2.0": "jsr:@gleasonator/policy@0.2.0",
"jsr:@gleasonator/policy@0.4.0": "jsr:@gleasonator/policy@0.4.0", "jsr:@gleasonator/policy@0.4.0": "jsr:@gleasonator/policy@0.4.0",
@ -20,6 +22,7 @@
"jsr:@nostrify/types@^0.30.0": "jsr:@nostrify/types@0.30.0", "jsr:@nostrify/types@^0.30.0": "jsr:@nostrify/types@0.30.0",
"jsr:@soapbox/kysely-deno-sqlite@^2.1.0": "jsr:@soapbox/kysely-deno-sqlite@2.2.0", "jsr:@soapbox/kysely-deno-sqlite@^2.1.0": "jsr:@soapbox/kysely-deno-sqlite@2.2.0",
"jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0",
"jsr:@std/assert@^0.213.1": "jsr:@std/assert@0.213.1",
"jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0", "jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0",
"jsr:@std/assert@^0.221.0": "jsr:@std/assert@0.221.0", "jsr:@std/assert@^0.221.0": "jsr:@std/assert@0.221.0",
"jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0",
@ -30,17 +33,22 @@
"jsr:@std/bytes@^1.0.2-rc.3": "jsr:@std/bytes@1.0.2", "jsr:@std/bytes@^1.0.2-rc.3": "jsr:@std/bytes@1.0.2",
"jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0",
"jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.2", "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.2",
"jsr:@std/encoding@0.213.1": "jsr:@std/encoding@0.213.1",
"jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0",
"jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3", "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3",
"jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3", "jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3",
"jsr:@std/fmt@0.213.1": "jsr:@std/fmt@0.213.1",
"jsr:@std/fmt@^0.221.0": "jsr:@std/fmt@0.221.0", "jsr:@std/fmt@^0.221.0": "jsr:@std/fmt@0.221.0",
"jsr:@std/fs@0.213.1": "jsr:@std/fs@0.213.1",
"jsr:@std/fs@^0.221.0": "jsr:@std/fs@0.221.0", "jsr:@std/fs@^0.221.0": "jsr:@std/fs@0.221.0",
"jsr:@std/fs@^0.229.3": "jsr:@std/fs@0.229.3", "jsr:@std/fs@^0.229.3": "jsr:@std/fs@0.229.3",
"jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.1", "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.1",
"jsr:@std/io@^0.224": "jsr:@std/io@0.224.4", "jsr:@std/io@^0.224": "jsr:@std/io@0.224.4",
"jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0", "jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0",
"jsr:@std/media-types@^0.224.1": "jsr:@std/media-types@0.224.1", "jsr:@std/media-types@^0.224.1": "jsr:@std/media-types@0.224.1",
"jsr:@std/path@0.213.1": "jsr:@std/path@0.213.1",
"jsr:@std/path@0.217": "jsr:@std/path@0.217.0", "jsr:@std/path@0.217": "jsr:@std/path@0.217.0",
"jsr:@std/path@^0.213.1": "jsr:@std/path@0.213.1",
"jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0", "jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0",
"jsr:@std/streams@^0.223.0": "jsr:@std/streams@0.223.0", "jsr:@std/streams@^0.223.0": "jsr:@std/streams@0.223.0",
"npm:@isaacs/ttlcache@^1.4.1": "npm:@isaacs/ttlcache@1.4.1", "npm:@isaacs/ttlcache@^1.4.1": "npm:@isaacs/ttlcache@1.4.1",
@ -84,6 +92,12 @@
"npm:zod@^3.23.8": "npm:zod@3.23.8" "npm:zod@^3.23.8": "npm:zod@3.23.8"
}, },
"jsr": { "jsr": {
"@b-fuze/deno-dom@0.1.47": {
"integrity": "270a888de91329f8ce3849211ece0ad97ce1e8b9a8a774f2bed2f43c8b0ffe8e",
"dependencies": [
"jsr:@denosaurs/plug@1.0.3"
]
},
"@bradenmacdonald/s3-lite-client@0.7.6": { "@bradenmacdonald/s3-lite-client@0.7.6": {
"integrity": "2b5976dca95d207dc88e23f9807e3eecbc441b0cf547dcda5784afe6668404d1", "integrity": "2b5976dca95d207dc88e23f9807e3eecbc441b0cf547dcda5784afe6668404d1",
"dependencies": [ "dependencies": [
@ -97,6 +111,15 @@
"jsr:@std/path@0.217" "jsr:@std/path@0.217"
] ]
}, },
"@denosaurs/plug@1.0.3": {
"integrity": "b010544e386bea0ff3a1d05e0c88f704ea28cbd4d753439c2f1ee021a85d4640",
"dependencies": [
"jsr:@std/encoding@0.213.1",
"jsr:@std/fmt@0.213.1",
"jsr:@std/fs@0.213.1",
"jsr:@std/path@0.213.1"
]
},
"@denosaurs/plug@1.0.6": { "@denosaurs/plug@1.0.6": {
"integrity": "6cf5b9daba7799837b9ffbe89f3450510f588fafef8115ddab1ff0be9cb7c1a7", "integrity": "6cf5b9daba7799837b9ffbe89f3450510f588fafef8115ddab1ff0be9cb7c1a7",
"dependencies": [ "dependencies": [
@ -216,6 +239,9 @@
"@soapbox/stickynotes@0.4.0": { "@soapbox/stickynotes@0.4.0": {
"integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec"
}, },
"@std/assert@0.213.1": {
"integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe"
},
"@std/assert@0.217.0": { "@std/assert@0.217.0": {
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
}, },
@ -253,15 +279,28 @@
"@std/dotenv@0.224.2": { "@std/dotenv@0.224.2": {
"integrity": "29081695357e4534696c9e986b2560be29c141ccf52daa32b6c20ff5b5c64ab9" "integrity": "29081695357e4534696c9e986b2560be29c141ccf52daa32b6c20ff5b5c64ab9"
}, },
"@std/encoding@0.213.1": {
"integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62"
},
"@std/encoding@0.221.0": { "@std/encoding@0.221.0": {
"integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45"
}, },
"@std/encoding@0.224.3": { "@std/encoding@0.224.3": {
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf"
}, },
"@std/fmt@0.213.1": {
"integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3"
},
"@std/fmt@0.221.0": { "@std/fmt@0.221.0": {
"integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a"
}, },
"@std/fs@0.213.1": {
"integrity": "fbcaf099f8a85c27ab0712b666262cda8fe6d02e9937bf9313ecaea39a22c501",
"dependencies": [
"jsr:@std/assert@^0.213.1",
"jsr:@std/path@^0.213.1"
]
},
"@std/fs@0.221.0": { "@std/fs@0.221.0": {
"integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286", "integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286",
"dependencies": [ "dependencies": [
@ -308,6 +347,12 @@
"@std/media-types@0.224.1": { "@std/media-types@0.224.1": {
"integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1" "integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1"
}, },
"@std/path@0.213.1": {
"integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673",
"dependencies": [
"jsr:@std/assert@^0.213.1"
]
},
"@std/path@0.217.0": { "@std/path@0.217.0": {
"integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11",
"dependencies": [ "dependencies": [
@ -1836,6 +1881,7 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@b-fuze/deno-dom@^0.1.47",
"jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
"jsr:@db/sqlite@^0.11.1", "jsr:@db/sqlite@^0.11.1",
"jsr:@hono/hono@^4.4.6", "jsr:@hono/hono@^4.4.6",

43
src/utils/favicon.ts Normal file
View file

@ -0,0 +1,43 @@
import { DOMParser } from '@b-fuze/deno-dom/native';
import Debug from '@soapbox/stickynotes/debug';
import tldts from 'tldts';
import { SimpleLRU } from '@/utils/SimpleLRU.ts';
import { Time } from '@/utils/time.ts';
import { fetchWorker } from '@/workers/fetch.ts';
const debug = Debug('ditto:favicon');
const faviconCache = new SimpleLRU<string, URL>(
async (key, { signal }) => {
debug(`Fetching favicon ${key}`);
const tld = tldts.parse(key);
if (!tld.isIcann || tld.isIp || tld.isPrivate) {
throw new Error(`Invalid favicon domain: ${key}`);
}
const rootUrl = new URL('/', `https://${key}/`);
const response = await fetchWorker(rootUrl, { signal });
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const link = doc.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
if (link) {
const href = link.getAttribute('href');
if (href) {
try {
return new URL(href);
} catch {
return new URL(href, rootUrl);
}
}
}
throw new Error(`Favicon not found: ${key}`);
},
{ max: 500, ttl: Time.hours(1) },
);
export { faviconCache };

View file

@ -8,6 +8,7 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { getLnurl } from '@/utils/lnurl.ts'; import { getLnurl } from '@/utils/lnurl.ts';
import { parseAndVerifyNip05 } from '@/utils/nip05.ts'; import { parseAndVerifyNip05 } from '@/utils/nip05.ts';
import { getTagSet } from '@/utils/tags.ts'; import { getTagSet } from '@/utils/tags.ts';
import { faviconCache } from '@/utils/favicon.ts';
import { nostrDate, nostrNow } from '@/utils.ts'; import { nostrDate, nostrNow } from '@/utils.ts';
import { renderEmojis } from '@/views/mastodon/emojis.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts';
@ -44,6 +45,15 @@ async function renderAccount(
const parsed05 = await parseAndVerifyNip05(nip05, pubkey); const parsed05 = await parseAndVerifyNip05(nip05, pubkey);
const acct = parsed05?.handle || npub; const acct = parsed05?.handle || npub;
let favicon: URL | undefined;
if (parsed05?.domain) {
try {
favicon = await faviconCache.fetch(parsed05.domain);
} catch {
favicon = new URL('/favicon.ico', `https://${parsed05.domain}/`);
}
}
return { return {
id: pubkey, id: pubkey,
acct, acct,
@ -95,7 +105,7 @@ async function renderAccount(
is_local: parsed05?.domain === Conf.url.host, is_local: parsed05?.domain === Conf.url.host,
settings_store: undefined as unknown, settings_store: undefined as unknown,
tags: [...getTagSet(event.user?.tags ?? [], 't')], tags: [...getTagSet(event.user?.tags ?? [], 't')],
favicon: parsed05?.domain ? new URL('/favicon.ico', `https://${parsed05.domain}`).toString() : undefined, favicon: favicon?.toString(),
}, },
nostr: { nostr: {
pubkey, pubkey,