kopia lustrzana https://github.com/cloudflare/wildebeest
commit
0fd30524f5
|
@ -0,0 +1,59 @@
|
||||||
|
// https://docs.joinmastodon.org/methods/accounts/#get
|
||||||
|
|
||||||
|
import { actorURL, getActorById } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
||||||
|
import type { Handle } from 'wildebeest/backend/src/utils/parse'
|
||||||
|
import { queryAcct } from 'wildebeest/backend/src/webfinger/index'
|
||||||
|
import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
||||||
|
import { MastodonAccount } from '../types'
|
||||||
|
|
||||||
|
export async function getAccount(domain: string, accountId: string, db: D1Database): Promise<MastodonAccount | null> {
|
||||||
|
const handle = parseHandle(accountId)
|
||||||
|
|
||||||
|
if (handle.domain === null || (handle.domain !== null && handle.domain === domain)) {
|
||||||
|
// Retrieve the statuses from a local user
|
||||||
|
return getLocalAccount(domain, db, handle)
|
||||||
|
} else if (handle.domain !== null) {
|
||||||
|
// Retrieve the statuses of a remote actor
|
||||||
|
const acct = `${handle.localPart}@${handle.domain}`
|
||||||
|
return getRemoteAccount(handle, acct)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRemoteAccount(handle: Handle, acct: string): Promise<MastodonAccount | null> {
|
||||||
|
// TODO: using webfinger isn't the optimal implementation. We could cache
|
||||||
|
// the object in D1 and directly query the remote API, indicated by the actor's
|
||||||
|
// url field. For now, let's keep it simple.
|
||||||
|
const actor = await queryAcct(handle.domain!, acct)
|
||||||
|
if (actor === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return await loadExternalMastodonAccount(acct, actor, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLocalAccount(domain: string, db: D1Database, handle: Handle): Promise<MastodonAccount | null> {
|
||||||
|
const actorId = actorURL(adjustLocalHostDomain(domain), handle.localPart)
|
||||||
|
|
||||||
|
const actor = await getActorById(db, actorId)
|
||||||
|
if (actor === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return await loadLocalMastodonAccount(db, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if a domain is a localhost one ('localhost' or '127.x.x.x') and
|
||||||
|
* in that case replaces it with '0.0.0.0' (which is what we use for our local data)
|
||||||
|
*
|
||||||
|
* Note: only needed for local development
|
||||||
|
*
|
||||||
|
* @param domain the potentially localhost domain
|
||||||
|
* @returns the adjusted domain if it was a localhost one, the original domain otherwise
|
||||||
|
*/
|
||||||
|
function adjustLocalHostDomain(domain: string) {
|
||||||
|
return domain.replace(/^localhost$|^127(\.(?:\d){1,3}){3}$/, '0.0.0.0')
|
||||||
|
}
|
|
@ -231,7 +231,7 @@ export async function sanitizeObjectProperties(properties: unknown): Promise<APO
|
||||||
* See https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
* See https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
||||||
*/
|
*/
|
||||||
export async function sanitizeContent(unsafeContent: string): Promise<string> {
|
export async function sanitizeContent(unsafeContent: string): Promise<string> {
|
||||||
return await contentRewriter.transform(new Response(unsafeContent)).text()
|
return await getContentRewriter().transform(new Response(unsafeContent)).text()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,29 +240,35 @@ export async function sanitizeContent(unsafeContent: string): Promise<string> {
|
||||||
* This sanitization removes all HTML elements from the string leaving only the text content.
|
* This sanitization removes all HTML elements from the string leaving only the text content.
|
||||||
*/
|
*/
|
||||||
export async function sanitizeName(unsafeName: string): Promise<string> {
|
export async function sanitizeName(unsafeName: string): Promise<string> {
|
||||||
return await nameRewriter.transform(new Response(unsafeName)).text()
|
return await getNameRewriter().transform(new Response(unsafeName)).text()
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentRewriter = new HTMLRewriter()
|
function getContentRewriter() {
|
||||||
contentRewriter.on('*', {
|
const contentRewriter = new HTMLRewriter()
|
||||||
element(el) {
|
contentRewriter.on('*', {
|
||||||
if (!['p', 'span', 'br', 'a'].includes(el.tagName)) {
|
element(el) {
|
||||||
el.tagName = 'p'
|
if (!['p', 'span', 'br', 'a'].includes(el.tagName)) {
|
||||||
}
|
el.tagName = 'p'
|
||||||
|
}
|
||||||
|
|
||||||
if (el.hasAttribute('class')) {
|
if (el.hasAttribute('class')) {
|
||||||
const classes = el.getAttribute('class')!.split(/\s+/)
|
const classes = el.getAttribute('class')!.split(/\s+/)
|
||||||
const sanitizedClasses = classes.filter((c) =>
|
const sanitizedClasses = classes.filter((c) =>
|
||||||
/^(h|p|u|dt|e)-|^mention$|^hashtag$|^ellipsis$|^invisible$/.test(c)
|
/^(h|p|u|dt|e)-|^mention$|^hashtag$|^ellipsis$|^invisible$/.test(c)
|
||||||
)
|
)
|
||||||
el.setAttribute('class', sanitizedClasses.join(' '))
|
el.setAttribute('class', sanitizedClasses.join(' '))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
return contentRewriter
|
||||||
|
}
|
||||||
|
|
||||||
const nameRewriter = new HTMLRewriter()
|
function getNameRewriter() {
|
||||||
nameRewriter.on('*', {
|
const nameRewriter = new HTMLRewriter()
|
||||||
element(el) {
|
nameRewriter.on('*', {
|
||||||
el.removeAndKeepContent()
|
element(el) {
|
||||||
},
|
el.removeAndKeepContent()
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
return nameRewriter
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Link, useNavigate } from '@builder.io/qwik-city'
|
||||||
import { formatTimeAgo } from '~/utils/dateTime'
|
import { formatTimeAgo } from '~/utils/dateTime'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import type { Account, MastodonStatus } from '~/types'
|
import type { Account, MastodonStatus } from '~/types'
|
||||||
import styles from './index.scss?inline'
|
import styles from '../../utils/innerHtmlContent.scss?inline'
|
||||||
import { MediaGallery } from '../MediaGallery.tsx'
|
import { MediaGallery } from '../MediaGallery.tsx'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -45,7 +45,7 @@ export default component$((props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="leading-relaxed status-content" dangerouslySetInnerHTML={status.content} />
|
<div class="leading-relaxed inner-html-content" dangerouslySetInnerHTML={status.content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MediaGallery medias={status.media_attachments} />
|
<MediaGallery medias={status.media_attachments} />
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { MediaGallery } from '~/components/MediaGallery.tsx'
|
||||||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
||||||
|
|
||||||
export const statusLoader = loader$<
|
export const statusLoader = loader$<
|
||||||
{ DATABASE: D1Database; domain: string },
|
{ DATABASE: D1Database },
|
||||||
Promise<{ status: MastodonStatus; context: StatusContext }>
|
Promise<{ status: MastodonStatus; context: StatusContext }>
|
||||||
>(async ({ request, html, platform, params }) => {
|
>(async ({ request, html, platform, params }) => {
|
||||||
const domain = new URL(request.url).hostname
|
const domain = new URL(request.url).hostname
|
||||||
|
|
|
@ -1,23 +1,131 @@
|
||||||
import { component$ } from '@builder.io/qwik'
|
import { $, component$, useStyles$ } from '@builder.io/qwik'
|
||||||
import { loader$ } from '@builder.io/qwik-city'
|
import { loader$, useNavigate } from '@builder.io/qwik-city'
|
||||||
|
import { MastodonAccount } from 'wildebeest/backend/src/types'
|
||||||
|
import StickyHeader from '~/components/StickyHeader/StickyHeader'
|
||||||
|
import { formatDateTime } from '~/utils/dateTime'
|
||||||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
||||||
|
import { formatRoundedNumber } from '~/utils/numbers'
|
||||||
|
import styles from '../../../utils/innerHtmlContent.scss?inline'
|
||||||
|
import { getAccount } from 'wildebeest/backend/src/accounts/getAccount'
|
||||||
|
|
||||||
export const accountLoader = loader$(({ request, html }) => {
|
export async function getAccountDetails(
|
||||||
const params = new URL(request.url).searchParams
|
domain: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
db: D1Database,
|
||||||
const accountId = params.get('accountId')
|
accountId: string
|
||||||
|
): Promise<MastodonAccount | null> {
|
||||||
|
return await getAccount(domain, accountId, db)
|
||||||
|
}
|
||||||
|
|
||||||
html(404, getNotFoundHtml())
|
export const accountLoader = loader$<{ DATABASE: D1Database }, Promise<MastodonAccount>>(
|
||||||
|
async ({ platform, request, html }) => {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const domain = url.hostname
|
||||||
|
const accountId = url.pathname.split('/')[1]
|
||||||
|
|
||||||
// TODO: retrieve the account details from the backend
|
const account = await getAccountDetails(domain, platform.DATABASE, accountId)
|
||||||
const accountDetails = null
|
|
||||||
|
|
||||||
return accountDetails
|
if (!account) {
|
||||||
})
|
throw html(404, getNotFoundHtml())
|
||||||
|
}
|
||||||
|
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export default component$(() => {
|
export default component$(() => {
|
||||||
const accountDetails = accountLoader.use()
|
useStyles$(styles)
|
||||||
|
const nav = useNavigate()
|
||||||
|
|
||||||
// TODO: Implement the account view
|
const accountDetails = accountLoader.use().value
|
||||||
return <>{accountDetails.value && <div>account details</div>}</>
|
|
||||||
|
const goBack = $(() => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
window.history.back()
|
||||||
|
} else {
|
||||||
|
nav('/explore')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
name: 'Joined',
|
||||||
|
value: formatDateTime(accountDetails.created_at, false),
|
||||||
|
},
|
||||||
|
...accountDetails.fields,
|
||||||
|
]
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
name: 'Posts',
|
||||||
|
value: formatRoundedNumber(accountDetails.statuses_count),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Following',
|
||||||
|
value: formatRoundedNumber(accountDetails.following_count),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Followers',
|
||||||
|
value: formatRoundedNumber(accountDetails.followers_count),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const accountDomain = getAccountDomain(accountDetails)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StickyHeader>
|
||||||
|
<div class="flex justify-between items-center xl:rounded-t header bg-wildebeest-700">
|
||||||
|
<button class="text-semi no-underline text-wildebeest-vibrant-400 bg-transparent p-4" onClick$={goBack}>
|
||||||
|
<i class="fa fa-chevron-left mr-2 w-3 inline-block" />
|
||||||
|
<span class="hover:underline">Back</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</StickyHeader>
|
||||||
|
<div class="relative mb-16">
|
||||||
|
<img
|
||||||
|
src={accountDetails.header}
|
||||||
|
alt={`Header of ${accountDetails.display_name}`}
|
||||||
|
class="w-full h-40 object-cover bg-wildebeest-500"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
class="rounded h-24 w-24 absolute bottom-[-3rem] left-5 border-2 border-wildebeest-600"
|
||||||
|
src={accountDetails.avatar}
|
||||||
|
alt={`Avatar of ${accountDetails.display_name}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="px-5">
|
||||||
|
<h2 class="font-bold">{accountDetails.display_name}</h2>
|
||||||
|
<span class="block my-1 text-wildebeest-400">
|
||||||
|
@{accountDetails.acct}
|
||||||
|
{accountDomain && `@${accountDomain}`}
|
||||||
|
</span>
|
||||||
|
<div class="inner-html-content my-5" dangerouslySetInnerHTML={accountDetails.note} />
|
||||||
|
<dl class="mb-6 flex flex-col bg-wildebeest-800 border border-wildebeest-600 rounded-md">
|
||||||
|
{fields.map(({ name, value }) => (
|
||||||
|
<div class="border-b border-wildebeest-600 p-3 text-sm" key={name}>
|
||||||
|
<dt class="uppercase font-semibold text-wildebeest-500 opacity-80 mb-1">{name}</dt>
|
||||||
|
<dd class="inner-html-content opacity-80 text-wildebeest-200" dangerouslySetInnerHTML={value}></dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
<div class="pb-4 flex flex-wrap gap-5">
|
||||||
|
{stats.map(({ name, value }) => (
|
||||||
|
<div class="flex gap-1" key={name}>
|
||||||
|
<span class="font-semibold">{value}</span>
|
||||||
|
<span class="text-wildebeest-500">{name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function getAccountDomain(account: MastodonAccount): string | null {
|
||||||
|
try {
|
||||||
|
const url = new URL(account.url)
|
||||||
|
return url.hostname || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -40,9 +40,9 @@ export const formatTimeAgo = (date: Date) => {
|
||||||
return `${roundTo(YEAR)}y`
|
return `${roundTo(YEAR)}y`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDateTime = (isoString: string) => {
|
export const formatDateTime = (isoString: string, includeTime = true) => {
|
||||||
const date = new Date(isoString)
|
const date = new Date(isoString)
|
||||||
const dateFormatter = Intl.DateTimeFormat('en', { month: 'short', day: 'numeric', year: 'numeric' })
|
const dateFormatter = Intl.DateTimeFormat('en', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
const timeFormatter = Intl.DateTimeFormat('en', { timeStyle: 'short' })
|
const timeFormatter = Intl.DateTimeFormat('en', { timeStyle: 'short' })
|
||||||
return `${dateFormatter.format(date)}, ${timeFormatter.format(date)}`
|
return [dateFormatter.format(date), ...(includeTime ? [timeFormatter.format(date)] : [])].join(', ')
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// defined by the client (set using dangerouslySetInnerHTML)
|
// defined by the client (set using dangerouslySetInnerHTML)
|
||||||
// (thus wee cannot rely on Tailwind for such content)
|
// (thus wee cannot rely on Tailwind for such content)
|
||||||
|
|
||||||
.status-content {
|
.inner-html-content {
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--wildebeest-vibrant-color-400);
|
color: var(--wildebeest-vibrant-color-400);
|
||||||
|
@ -17,28 +17,28 @@
|
||||||
p {
|
p {
|
||||||
margin-bottom: theme('spacing.4');
|
margin-bottom: theme('spacing.4');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.invisible {
|
.invisible {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis {
|
.ellipsis {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis::after {
|
.ellipsis::after {
|
||||||
content: "...";
|
content: "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-link {
|
.status-link {
|
||||||
color: var(--wildebeest-vibrant-color-200);
|
color: var(--wildebeest-vibrant-color-200);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { fetch } from 'undici'
|
||||||
|
|
||||||
|
describe('Account page', () => {
|
||||||
|
it('should display the basic information of an account', async () => {
|
||||||
|
const response = await fetch(`http://0.0.0.0:6868/@BethanyBlack`)
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
const body = await response.text()
|
||||||
|
expect(body).toMatch(/<img [^<>]*alt="Avatar of Bethany Black"[^<>]*>/)
|
||||||
|
expect(body).toMatch(/<h2 [^<>]*>Bethany Black<\/h2>/)
|
||||||
|
expect(body).toMatch(
|
||||||
|
/<div [^<>]*><dt [^<>]*>Joined<\/dt>[^<>]*<dd [^<>]*>[A-Z][a-z]{2} \d{1,2}, \d{4}<\/dd>[^<>]*<\/div>/
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(body).toMatch(
|
||||||
|
/<div [^<>]*><dt [^<>]*>Joined<\/dt>[^<>]*<dd [^<>]*>[A-Z][a-z]{2} \d{1,2}, \d{4}<\/dd>[^<>]*<\/div>/
|
||||||
|
)
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ name: 'Posts', value: 1 },
|
||||||
|
{ name: 'Posts', value: 1 },
|
||||||
|
{ name: 'Following', value: 0 },
|
||||||
|
{ name: 'Followers', value: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
stats.forEach(({ name, value }) => {
|
||||||
|
const regex = new RegExp(`<span [^<>]*>${value}</span>[^<>]*<span [^<>]*>${name}</span>`)
|
||||||
|
expect(body).toMatch(regex)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,14 +1,9 @@
|
||||||
// https://docs.joinmastodon.org/methods/accounts/#get
|
// https://docs.joinmastodon.org/methods/accounts/#get
|
||||||
|
|
||||||
import { cors } from 'wildebeest/backend/src/utils/cors'
|
import { cors } from 'wildebeest/backend/src/utils/cors'
|
||||||
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
|
|
||||||
import { getActorById } from 'wildebeest/backend/src/activitypub/actors'
|
|
||||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
import { getAccount } from 'wildebeest/backend/src/accounts/getAccount'
|
||||||
import type { Handle } from 'wildebeest/backend/src/utils/parse'
|
|
||||||
import { queryAcct } from 'wildebeest/backend/src/webfinger/index'
|
|
||||||
import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...cors(),
|
...cors(),
|
||||||
|
@ -21,41 +16,11 @@ export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request,
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleRequest(domain: string, id: string, db: D1Database): Promise<Response> {
|
export async function handleRequest(domain: string, id: string, db: D1Database): Promise<Response> {
|
||||||
const handle = parseHandle(id)
|
const account = await getAccount(domain, id, db)
|
||||||
|
|
||||||
if (handle.domain === null || (handle.domain !== null && handle.domain === domain)) {
|
if (account) {
|
||||||
// Retrieve the statuses from a local user
|
return new Response(JSON.stringify(account), { headers })
|
||||||
return getLocalAccount(domain, db, handle)
|
|
||||||
} else if (handle.domain !== null) {
|
|
||||||
// Retrieve the statuses of a remote actor
|
|
||||||
const acct = `${handle.localPart}@${handle.domain}`
|
|
||||||
return getRemoteAccount(handle, acct)
|
|
||||||
} else {
|
} else {
|
||||||
return new Response('', { status: 403 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRemoteAccount(handle: Handle, acct: string): Promise<Response> {
|
|
||||||
// TODO: using webfinger isn't the optimal implementation. We could cache
|
|
||||||
// the object in D1 and directly query the remote API, indicated by the actor's
|
|
||||||
// url field. For now, let's keep it simple.
|
|
||||||
const actor = await queryAcct(handle.domain!, acct)
|
|
||||||
if (actor === null) {
|
|
||||||
return new Response('', { status: 404 })
|
return new Response('', { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await loadExternalMastodonAccount(acct, actor, true)
|
|
||||||
return new Response(JSON.stringify(res), { headers })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLocalAccount(domain: string, db: D1Database, handle: Handle): Promise<Response> {
|
|
||||||
const actorId = actorURL(domain, handle.localPart)
|
|
||||||
|
|
||||||
const actor = await getActorById(db, actorId)
|
|
||||||
if (actor === null) {
|
|
||||||
return new Response('', { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await loadLocalMastodonAccount(db, actor)
|
|
||||||
return new Response(JSON.stringify(res), { headers })
|
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue