diff --git a/backend/src/activitypub/actors/index.ts b/backend/src/activitypub/actors/index.ts index 08d311b..2ec74ef 100644 --- a/backend/src/activitypub/actors/index.ts +++ b/backend/src/activitypub/actors/index.ts @@ -34,6 +34,8 @@ export interface Actor extends APObject { following: URL followers: URL + alsoKnownAs?: string + [emailSymbol]: string } @@ -200,6 +202,16 @@ export async function updateActorProperty(db: D1Database, actorId: URL, key: str } } +export async function setActorAlias(db: D1Database, actorId: URL, alias: URL) { + const { success, error } = await db + .prepare(`UPDATE actors SET properties=json_set(properties, '$.alsoKnownAs', json_array(?)) WHERE id=?`) + .bind(alias.toString(), actorId.toString()) + .run() + if (!success) { + throw new Error('SQL error: ' + error) + } +} + export async function getActorById(db: D1Database, id: URL): Promise { const stmt = db.prepare('SELECT * FROM actors WHERE id=?').bind(id.toString()) const { results } = await stmt.all() diff --git a/backend/test/wildebeest/settings.spec.ts b/backend/test/wildebeest/settings.spec.ts new file mode 100644 index 0000000..3dc923a --- /dev/null +++ b/backend/test/wildebeest/settings.spec.ts @@ -0,0 +1,58 @@ +import { makeDB } from '../utils' +import { strict as assert } from 'node:assert/strict' +import { createPerson, getActorById } from 'wildebeest/backend/src/activitypub/actors' +import * as account_alias from 'wildebeest/functions/api/wb/settings/account/alias' + +const domain = 'cloudflare.com' +const userKEK = 'test_kek22' + +describe('Wildebeest', () => { + describe('Settings', () => { + test('add account alias', async () => { + const db = await makeDB() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + + globalThis.fetch = async (input: RequestInfo) => { + if (input.toString() === 'https://example.com/.well-known/webfinger?resource=acct%3Atest%40example.com') { + return new Response( + JSON.stringify({ + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: 'https://social.com/someone', + }, + ], + }) + ) + } + + if (input.toString() === 'https://social.com/someone') { + return new Response( + JSON.stringify({ + id: 'https://social.com/someone', + }) + ) + } + + throw new Error('unexpected request to ' + input) + } + + const alias = 'test@example.com' + + const req = new Request('https://example.com', { + method: 'POST', + body: JSON.stringify({ alias }), + }) + const res = await account_alias.handleRequestPost(db, req, actor) + assert.equal(res.status, 201) + + // Ensure the actor has the alias set + const newActor = await getActorById(db, actor.id) + assert(newActor) + assert(newActor.alsoKnownAs) + assert.equal(newActor.alsoKnownAs.length, 1) + assert.equal(newActor.alsoKnownAs[0], 'https://social.com/someone') + }) + }) +}) diff --git a/functions/ap/users/[id].ts b/functions/ap/users/[id].ts index 9aa4ed5..7eaf06f 100644 --- a/functions/ap/users/[id].ts +++ b/functions/ap/users/[id].ts @@ -35,6 +35,10 @@ export async function handleRequest(domain: string, db: D1Database, id: string): { toot: 'http://joinmastodon.org/ns#', discoverable: 'toot:discoverable', + alsoKnownAs: { + '@id': 'as:alsoKnownAs', + '@type': '@id', + }, }, ], diff --git a/functions/api/wb/settings/account/alias.ts b/functions/api/wb/settings/account/alias.ts new file mode 100644 index 0000000..8edbb2e --- /dev/null +++ b/functions/api/wb/settings/account/alias.ts @@ -0,0 +1,34 @@ +import type { Env } from 'wildebeest/backend/src/types/env' +import { setActorAlias } from 'wildebeest/backend/src/activitypub/actors' +import type { ContextData } from 'wildebeest/backend/src/types/context' +import type { Actor } from 'wildebeest/backend/src/activitypub/actors' +import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { queryAcct } from 'wildebeest/backend/src/webfinger' +import * as errors from 'wildebeest/backend/src/errors' + +export const onRequestPost: PagesFunction = async ({ env, request, data }) => { + return handleRequestPost(env.DATABASE, request, data.connectedActor) +} + +type AddAliasRequest = { + alias: string +} + +export async function handleRequestPost(db: D1Database, request: Request, connectedActor: Actor): Promise { + const body = await request.json() + + const handle = parseHandle(body.alias) + const acct = `${handle.localPart}@${handle.domain}` + if (handle.domain === null) { + console.warn("account migration within an instance isn't supported") + return new Response('', { status: 400 }) + } + const actor = await queryAcct(handle.domain, acct) + if (actor === null) { + return errors.resourceNotFound('actor', acct) + } + + await setActorAlias(db, connectedActor.id, actor.id) + + return new Response('', { status: 201 }) +}