diff --git a/backend/src/mastodon/microformats.ts b/backend/src/mastodon/microformats.ts index 1cc2f8e..c724958 100644 --- a/backend/src/mastodon/microformats.ts +++ b/backend/src/mastodon/microformats.ts @@ -1,4 +1,6 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import type { Actor } from 'wildebeest/backend/src/activitypub/actors' +import { urlToHandle } from 'wildebeest/backend/src/utils/handle' function tag(name: string, content: string, attrs: Record = {}): string { let htmlAttrs = '' @@ -13,18 +15,24 @@ const linkRegex = /(^|\s|\b)(https?:\/\/[-\w@:%._+~#=]{2,256}\.[a-z]{2,6}\b(?:[- const mentionedEmailRegex = /(^|\s|\b|\W)@(\w+(?:[.-]?\w+)+@\w+(?:[.-]?\w+)+(?:\.\w{2,63})+)(\b|\s|$)/g /// Transform a text status into a HTML status; enriching it with links / mentions. -export function enrichStatus(status: string): string { +export function enrichStatus(status: string, mentions: Array): string { const enrichedStatus = status .replace( linkRegex, (_, matchPrefix: string, link: string, matchSuffix: string) => `${matchPrefix}${getLinkAnchor(link)}${matchSuffix}` ) - .replace( - mentionedEmailRegex, - (_, matchPrefix: string, email: string, matchSuffix: string) => - `${matchPrefix}${getMentionSpan(email)}${matchSuffix}` - ) + .replace(mentionedEmailRegex, (_, matchPrefix: string, email: string, matchSuffix: string) => { + // ensure that the match is part of the mentions array + for (let i = 0, len = mentions.length; i < len; i++) { + if (email === urlToHandle(mentions[i].id)) { + return `${matchPrefix}${getMentionSpan(email)}${matchSuffix}` + } + } + + // otherwise the match isn't valid and we don't add HTML + return `${matchPrefix}${email}${matchSuffix}` + }) return tag('p', enrichedStatus) } diff --git a/backend/src/webfinger/index.ts b/backend/src/webfinger/index.ts index dc79401..8f65d9e 100644 --- a/backend/src/webfinger/index.ts +++ b/backend/src/webfinger/index.ts @@ -21,20 +21,21 @@ export async function queryAcct(domain: string, acct: string): Promise { const params = new URLSearchParams({ resource: `acct:${acct}` }) - let res + let data: WebFingerResponse try { const url = new URL('/.well-known/webfinger?' + params, 'https://' + domain) console.log('query', url.href) - res = await fetch(url, { headers }) + const res = await fetch(url, { headers }) if (!res.ok) { throw new Error(`WebFinger API returned: ${res.status}`) } + + data = await res.json() } catch (err) { console.warn('failed to query WebFinger:', err) return null } - const data = await res.json() for (let i = 0, len = data.links.length; i < len; i++) { const link = data.links[i] if (link.rel === 'self' && link.type === 'application/activity+json') { diff --git a/backend/test/mastodon.spec.ts b/backend/test/mastodon.spec.ts index 5ed498d..3f5e2b3 100644 --- a/backend/test/mastodon.spec.ts +++ b/backend/test/mastodon.spec.ts @@ -167,7 +167,7 @@ describe('Mastodon APIs', () => { }) describe('Microformats', () => { - test('convert mentions to HTML', () => { + test('convert mentions to HTML', async () => { const mentionsToTest = [ { mention: '@sven2@example.com', @@ -195,18 +195,34 @@ describe('Mastodon APIs', () => { '@testey', }, ] - mentionsToTest.forEach(({ mention, expectedMentionSpan }) => { - assert.equal(enrichStatus(`hey ${mention} hi`), `

hey ${expectedMentionSpan} hi

`) - assert.equal(enrichStatus(`${mention} hi`), `

${expectedMentionSpan} hi

`) - assert.equal(enrichStatus(`${mention}\n\thein`), `

${expectedMentionSpan}\n\thein

`) - assert.equal(enrichStatus(`hey ${mention}`), `

hey ${expectedMentionSpan}

`) - assert.equal(enrichStatus(`${mention}`), `

${expectedMentionSpan}

`) - assert.equal(enrichStatus(`@!@£${mention}!!!`), `

@!@£${expectedMentionSpan}!!!

`) - }) + + for (let i = 0, len = mentionsToTest.length; i < len; i++) { + const { mention, expectedMentionSpan } = mentionsToTest[i] + + // List of mentioned actors, only the `id` is required so we can hack together an Actor + const mentions: any = [ + { id: new URL('https://example.com/sven2') }, + { id: new URL('https://example.eng.com/test') }, + { id: new URL('https://example.eng.co.uk/test.a.b.c-d') }, + { id: new URL('https://123456.abcdef/testey') }, + { id: new URL('https://123456.test.testey.abcdef/testey') }, + ] + + assert.equal(enrichStatus(`hey ${mention} hi`, mentions), `

hey ${expectedMentionSpan} hi

`) + assert.equal(enrichStatus(`${mention} hi`, mentions), `

${expectedMentionSpan} hi

`) + assert.equal(enrichStatus(`${mention}\n\thein`, mentions), `

${expectedMentionSpan}\n\thein

`) + assert.equal(enrichStatus(`hey ${mention}`, mentions), `

hey ${expectedMentionSpan}

`) + assert.equal(enrichStatus(`${mention}`, mentions), `

${expectedMentionSpan}

`) + assert.equal(enrichStatus(`@!@£${mention}!!!`, mentions), `

@!@£${expectedMentionSpan}!!!

`) + } }) test('handle invalid mention', () => { - assert.equal(enrichStatus('hey @#-...@example.com'), '

hey @#-...@example.com

') + assert.equal(enrichStatus('hey @#-...@example.com', []), '

hey @#-...@example.com

') + }) + + test('mention to invalid user', () => { + assert.equal(enrichStatus('hey test@example.com', []), '

hey test@example.com

') }) test('convert links to HTML', () => { @@ -222,11 +238,11 @@ describe('Mastodon APIs', () => { linksToTest.forEach((link) => { const url = new URL(link) const urlDisplayText = `${url.hostname}${url.pathname}` - assert.equal(enrichStatus(`hey ${link} hi`), `

hey ${urlDisplayText} hi

`) - assert.equal(enrichStatus(`${link} hi`), `

${urlDisplayText} hi

`) - assert.equal(enrichStatus(`hey ${link}`), `

hey ${urlDisplayText}

`) - assert.equal(enrichStatus(`${link}`), `

${urlDisplayText}

`) - assert.equal(enrichStatus(`@!@£${link}!!!`), `

@!@£${urlDisplayText}!!!

`) + assert.equal(enrichStatus(`hey ${link} hi`, []), `

hey ${urlDisplayText} hi

`) + assert.equal(enrichStatus(`${link} hi`, []), `

${urlDisplayText} hi

`) + assert.equal(enrichStatus(`hey ${link}`, []), `

hey ${urlDisplayText}

`) + assert.equal(enrichStatus(`${link}`, []), `

${urlDisplayText}

`) + assert.equal(enrichStatus(`@!@£${link}!!!`, []), `

@!@£${urlDisplayText}!!!

`) }) }) }) diff --git a/backend/test/mastodon/statuses.spec.ts b/backend/test/mastodon/statuses.spec.ts index f10ab64..db26dc7 100644 --- a/backend/test/mastodon/statuses.spec.ts +++ b/backend/test/mastodon/statuses.spec.ts @@ -457,6 +457,11 @@ describe('Mastodon APIs', () => { }) ) } + if ( + input.toString() === 'https://cloudflare.com/.well-known/webfinger?resource=acct%3Ano-json%40cloudflare.com' + ) { + return new Response('not json', { status: 200 }) + } if (input.toString() === 'https://instance.horse/users/sven') { return new Response( @@ -496,7 +501,7 @@ describe('Mastodon APIs', () => { } { - const mentions = await getMentions('unknown@actor.com', domain) + const mentions = await getMentions('no-json@actor.com', domain) assert.equal(mentions.length, 0) } @@ -524,6 +529,11 @@ describe('Mastodon APIs', () => { assert.equal(mentions.length, 1) assert.equal(mentions[0].id.toString(), 'https://' + domain + '/users/sven') } + + { + const mentions = await getMentions('

@unknown

', domain) + assert.equal(mentions.length, 0) + } }) test('get status count likes', async () => { diff --git a/functions/api/v1/statuses.ts b/functions/api/v1/statuses.ts index 75da97b..e1912b4 100644 --- a/functions/api/v1/statuses.ts +++ b/functions/api/v1/statuses.ts @@ -107,12 +107,12 @@ export async function handleRequest( const hashtags = getHashtags(body.status) - const content = enrichStatus(body.status) const mentions = await getMentions(body.status, domain) if (mentions.length > 0) { extraProperties.tag = mentions.map(newMention) } + const content = enrichStatus(body.status, mentions) const note = await createStatus(domain, db, connectedActor, content, mediaAttachments, extraProperties) if (hashtags.length > 0) {