From e539a29775833a070fde51a16532593c8d8ef5a5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Aug 2024 09:21:12 -0500 Subject: [PATCH 1/2] Fetch favicon from NIP-05 domain --- deno.json | 1 + deno.lock | 46 ++++++++++++++++++++++++++++++++++ src/utils/favicon.ts | 43 +++++++++++++++++++++++++++++++ src/views/mastodon/accounts.ts | 12 ++++++++- 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/utils/favicon.ts diff --git a/deno.json b/deno.json index 5f230172..96c86b19 100644 --- a/deno.json +++ b/deno.json @@ -24,6 +24,7 @@ "exclude": ["./public"], "imports": { "@/": "./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", "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@hono/hono": "jsr:@hono/hono@^4.4.6", diff --git a/deno.lock b/deno.lock index d1459554..68c03b53 100644 --- a/deno.lock +++ b/deno.lock @@ -2,9 +2,11 @@ "version": "3", "packages": { "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:@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.0.3": "jsr:@denosaurs/plug@1.0.3", "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.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:@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:@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.221.0": "jsr:@std/assert@0.221.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/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", "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.224.0": "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/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.229.3": "jsr:@std/fs@0.229.3", "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/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/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.213.1": "jsr:@std/path@0.213.1", "jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.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", @@ -84,6 +92,12 @@ "npm:zod@^3.23.8": "npm:zod@3.23.8" }, "jsr": { + "@b-fuze/deno-dom@0.1.47": { + "integrity": "270a888de91329f8ce3849211ece0ad97ce1e8b9a8a774f2bed2f43c8b0ffe8e", + "dependencies": [ + "jsr:@denosaurs/plug@1.0.3" + ] + }, "@bradenmacdonald/s3-lite-client@0.7.6": { "integrity": "2b5976dca95d207dc88e23f9807e3eecbc441b0cf547dcda5784afe6668404d1", "dependencies": [ @@ -97,6 +111,15 @@ "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": { "integrity": "6cf5b9daba7799837b9ffbe89f3450510f588fafef8115ddab1ff0be9cb7c1a7", "dependencies": [ @@ -216,6 +239,9 @@ "@soapbox/stickynotes@0.4.0": { "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" }, + "@std/assert@0.213.1": { + "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe" + }, "@std/assert@0.217.0": { "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" }, @@ -253,15 +279,28 @@ "@std/dotenv@0.224.2": { "integrity": "29081695357e4534696c9e986b2560be29c141ccf52daa32b6c20ff5b5c64ab9" }, + "@std/encoding@0.213.1": { + "integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62" + }, "@std/encoding@0.221.0": { "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" }, "@std/encoding@0.224.3": { "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" }, + "@std/fmt@0.213.1": { + "integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3" + }, "@std/fmt@0.221.0": { "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": { "integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286", "dependencies": [ @@ -308,6 +347,12 @@ "@std/media-types@0.224.1": { "integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1" }, + "@std/path@0.213.1": { + "integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673", + "dependencies": [ + "jsr:@std/assert@^0.213.1" + ] + }, "@std/path@0.217.0": { "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", "dependencies": [ @@ -1836,6 +1881,7 @@ }, "workspace": { "dependencies": [ + "jsr:@b-fuze/deno-dom@^0.1.47", "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "jsr:@db/sqlite@^0.11.1", "jsr:@hono/hono@^4.4.6", diff --git a/src/utils/favicon.ts b/src/utils/favicon.ts new file mode 100644 index 00000000..569a1e35 --- /dev/null +++ b/src/utils/favicon.ts @@ -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( + 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 }; diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 2a7c6b25..d1893f07 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -8,6 +8,7 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getLnurl } from '@/utils/lnurl.ts'; import { parseAndVerifyNip05 } from '@/utils/nip05.ts'; import { getTagSet } from '@/utils/tags.ts'; +import { faviconCache } from '@/utils/favicon.ts'; import { nostrDate, nostrNow } from '@/utils.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts'; @@ -44,6 +45,15 @@ async function renderAccount( const parsed05 = await parseAndVerifyNip05(nip05, pubkey); 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 { id: pubkey, acct, @@ -95,7 +105,7 @@ async function renderAccount( is_local: parsed05?.domain === Conf.url.host, settings_store: undefined as unknown, tags: [...getTagSet(event.user?.tags ?? [], 't')], - favicon: parsed05?.domain ? new URL('/favicon.ico', `https://${parsed05.domain}`).toString() : undefined, + favicon: favicon?.toString(), }, nostr: { pubkey, From 8370b250a2c697ff115f36b14bcfa780d0a4f34e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Aug 2024 09:25:48 -0500 Subject: [PATCH 2/2] Add a signal to renderAccount --- src/views/mastodon/accounts.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index d1893f07..b3efdc82 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -19,6 +19,7 @@ interface ToAccountOpts { async function renderAccount( event: Omit, opts: ToAccountOpts = {}, + signal = AbortSignal.timeout(3000), ): Promise { const { withSource = false } = opts; const { pubkey } = event; @@ -42,13 +43,13 @@ async function renderAccount( } = n.json().pipe(n.metadata()).catch({}).parse(event.content); const npub = nip19.npubEncode(pubkey); - const parsed05 = await parseAndVerifyNip05(nip05, pubkey); + const parsed05 = await parseAndVerifyNip05(nip05, pubkey, signal); const acct = parsed05?.handle || npub; let favicon: URL | undefined; if (parsed05?.domain) { try { - favicon = await faviconCache.fetch(parsed05.domain); + favicon = await faviconCache.fetch(parsed05.domain, { signal }); } catch { favicon = new URL('/favicon.ico', `https://${parsed05.domain}/`); }