kopia lustrzana https://github.com/cloudflare/wildebeest
Merge pull request #169 from cloudflare/sven/status-microformats
Enrich status with HTML Microformatspull/170/head
commit
8275f653f4
|
@ -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)
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { makeDB, assertCORS, assertJSON, assertCache, createTestClient } from '.
|
||||||
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { createSubscription } from '../src/mastodon/subscription'
|
import { createSubscription } from '../src/mastodon/subscription'
|
||||||
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
|
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
|
||||||
|
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
|
||||||
|
|
||||||
const userKEK = 'test_kek'
|
const userKEK = 'test_kek'
|
||||||
const domain = 'cloudflare.com'
|
const domain = 'cloudflare.com'
|
||||||
|
@ -281,4 +282,21 @@ describe('Mastodon APIs', () => {
|
||||||
const data = await res.json<any>()
|
const data = await res.json<any>()
|
||||||
assert.equal(data.length, 0)
|
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>')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -85,7 +85,7 @@ describe('Mastodon APIs', () => {
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.first<{ content: string; original_actor_id: URL; original_object_id: unknown }>()
|
.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_actor_id.toString(), actor.id.toString())
|
||||||
assert.equal(row.original_object_id, null)
|
assert.equal(row.original_object_id, null)
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,6 +21,7 @@ import type { Visibility } from 'wildebeest/backend/src/types'
|
||||||
import { toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status'
|
import { toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status'
|
||||||
import type { Cache } from 'wildebeest/backend/src/cache'
|
import type { Cache } from 'wildebeest/backend/src/cache'
|
||||||
import { cacheFromEnv } from 'wildebeest/backend/src/cache'
|
import { cacheFromEnv } from 'wildebeest/backend/src/cache'
|
||||||
|
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
|
||||||
|
|
||||||
type StatusCreate = {
|
type StatusCreate = {
|
||||||
status: string
|
status: string
|
||||||
|
@ -87,7 +88,8 @@ export async function handleRequest(
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = new URL(request.url).hostname
|
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) {
|
if (inReplyToObject !== null) {
|
||||||
// after the status has been created, record the reply.
|
// after the status has been created, record the reply.
|
||||||
|
|
Ładowanie…
Reference in New Issue