Merge pull request #169 from cloudflare/sven/status-microformats

Enrich status with HTML Microformats
pull/170/head
Sven Sauleau 2023-02-01 16:14:03 +00:00 zatwierdzone przez GitHub
commit 8275f653f4
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 96 dodań i 2 usunięć

Wyświetl plik

@ -0,0 +1,74 @@
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
function tag(name: string, content: string, attrs: Record<string, string> = {}): string {
let htmlAttrs = ''
for (const [key, value] of Object.entries(attrs)) {
htmlAttrs += ` ${key}="${value}"`
}
return `<${name}${htmlAttrs}>${content}</${name}>`
}
/// Transform a text status into a HTML status; enriching it with links / mentions.
export function enrichStatus(status: string): string {
let out = ''
let state = 'normal'
let buffer = ''
for (let i = 0, len = status.length; i < len; i++) {
const char = status[i]
if (char === '@') {
state = 'mention'
}
if (status.slice(i, i + 5) === 'http:' || status.slice(i, i + 6) === 'https:') {
state = 'link'
}
if (state === 'link') {
if (char === ' ') {
try {
const url = new URL(buffer)
buffer = ''
out += tag('a', url.hostname + url.pathname, { href: url.href })
} catch (err: unknown) {
console.warn('failed to parse link', err)
out += buffer
}
out += char
state = 'normal'
} else {
buffer += char
}
continue
}
if (state === 'mention') {
if (char === ' ') {
const handle = parseHandle(buffer)
buffer = ''
// TODO: the link to the profile is a guess, we could rely on
// the cached Actors to find the right link.
const linkToProfile = `https://${handle.domain}/@${handle.localPart}`
const mention = '@' + tag('span', handle.localPart)
out += tag('span', tag('a', mention, { href: linkToProfile, class: 'u-url mention' }), { class: 'h-card' })
out += char
state = 'normal'
} else {
buffer += char
}
continue
}
if (state === 'normal') {
out += char
continue
}
}
return tag('p', out)
}

Wyświetl plik

@ -11,6 +11,7 @@ import { makeDB, assertCORS, assertJSON, assertCache, createTestClient } from '.
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { createSubscription } from '../src/mastodon/subscription'
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
const userKEK = 'test_kek'
const domain = 'cloudflare.com'
@ -281,4 +282,21 @@ describe('Mastodon APIs', () => {
const data = await res.json<any>()
assert.equal(data.length, 0)
})
describe('Microformats', () => {
test('convert mentions to HTML', () => {
const status = 'hey @test@example.com hi'
assert.equal(
enrichStatus(status),
'<p>hey <span class="h-card"><a href="https://example.com/@test" class="u-url mention">@<span>test</span></a></span> hi</p>'
)
})
test('convert links to HTML', () => {
const status = 'hey https://cloudflare.com/abc hi'
assert.equal(enrichStatus(status), '<p>hey <a href="https://cloudflare.com/abc">cloudflare.com/abc</a> hi</p>')
})
})
})

Wyświetl plik

@ -85,7 +85,7 @@ describe('Mastodon APIs', () => {
`
)
.first<{ content: string; original_actor_id: URL; original_object_id: unknown }>()
assert.equal(row.content, 'my status <p>evil</p>') // note the sanitization
assert.equal(row.content, '<p>my status <p>evil</p></p>') // note the sanitization
assert.equal(row.original_actor_id.toString(), actor.id.toString())
assert.equal(row.original_object_id, null)
})

Wyświetl plik

@ -21,6 +21,7 @@ import type { Visibility } from 'wildebeest/backend/src/types'
import { toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status'
import type { Cache } from 'wildebeest/backend/src/cache'
import { cacheFromEnv } from 'wildebeest/backend/src/cache'
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
type StatusCreate = {
status: string
@ -87,7 +88,8 @@ export async function handleRequest(
}
const domain = new URL(request.url).hostname
const note = await createStatus(domain, db, connectedActor, body.status, mediaAttachments, extraProperties)
const content = enrichStatus(body.status)
const note = await createStatus(domain, db, connectedActor, content, mediaAttachments, extraProperties)
if (inReplyToObject !== null) {
// after the status has been created, record the reply.