diff --git a/backend/src/activitypub/activities/delete.ts b/backend/src/activitypub/activities/delete.ts new file mode 100644 index 0000000..c5af0c9 --- /dev/null +++ b/backend/src/activitypub/activities/delete.ts @@ -0,0 +1,16 @@ +import type { APObject } from '../objects' +import type { Actor } from '../actors' +import type { Activity } from '.' +import * as activity from '.' + +const DELETE = 'Delete' + +export function create(domain: string, actor: Actor, object: APObject): Activity { + return { + '@context': ['https://www.w3.org/ns/activitystreams'], + id: activity.uri(domain), + type: DELETE, + actor: actor.id, + object, + } +} diff --git a/backend/src/activitypub/deliver.ts b/backend/src/activitypub/deliver.ts index 86e4f1d..bca0e7c 100644 --- a/backend/src/activitypub/deliver.ts +++ b/backend/src/activitypub/deliver.ts @@ -36,6 +36,9 @@ export async function deliverToActor(signingKey: CryptoKey, from: Actor, to: Act console.log(`${to.inbox} returned 200`) } +// TODO: eventually move this to the queue worker, the backend can send a message +// to a collection (followers) and the worker creates the indivual messages. More +// reliable and scalable. export async function deliverFollowers( db: D1Database, userKEK: string, diff --git a/backend/test/mastodon/statuses.spec.ts b/backend/test/mastodon/statuses.spec.ts index d0710a4..d95b34c 100644 --- a/backend/test/mastodon/statuses.spec.ts +++ b/backend/test/mastodon/statuses.spec.ts @@ -813,29 +813,32 @@ describe('Mastodon APIs', () => { test('delete non-existing status', async () => { const db = await makeDB() + const queue = makeQueue() const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') const mastodonId = 'abcd' - const res = await statuses_id.handleRequestDelete(db, mastodonId, actor, domain) + const res = await statuses_id.handleRequestDelete(db, mastodonId, actor, domain, userKEK, queue) assert.equal(res.status, 404) }) test('delete status from a different actor', async () => { const db = await makeDB() + const queue = makeQueue() const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com') const note = await createPublicNote(domain, db, 'note from actor2', actor2) - const res = await statuses_id.handleRequestDelete(db, note[mastodonIdSymbol]!, actor, domain) + const res = await statuses_id.handleRequestDelete(db, note[mastodonIdSymbol]!, actor, domain, userKEK, queue) assert.equal(res.status, 404) }) - test('delete status', async () => { + test('delete status remove DB rows', async () => { const db = await makeDB() + const queue = makeQueue() const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') const note = await createPublicNote(domain, db, 'note from actor', actor) await addObjectInOutbox(db, actor, note) - const res = await statuses_id.handleRequestDelete(db, note[mastodonIdSymbol]!, actor, domain) + const res = await statuses_id.handleRequestDelete(db, note[mastodonIdSymbol]!, actor, domain, userKEK, queue) assert.equal(res.status, 200) { @@ -847,5 +850,30 @@ describe('Mastodon APIs', () => { assert.equal(count, 0) } }) + + test('delete status sends to followers', async () => { + const db = await makeDB() + const queue = makeQueue() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com') + const actor3 = await createPerson(domain, db, userKEK, 'sven3@cloudflare.com') + const note = await createPublicNote(domain, db, 'note from actor', actor) + + await addFollowing(db, actor2, actor, 'not needed') + await acceptFollowing(db, actor2, actor) + await addFollowing(db, actor3, actor, 'not needed') + await acceptFollowing(db, actor3, actor) + + const res = await statuses_id.handleRequestDelete(db, note[mastodonIdSymbol]!, actor, domain, userKEK, queue) + assert.equal(res.status, 200) + + assert.equal(queue.messages.length, 2) + assert.equal(queue.messages[0].activity.type, 'Delete') + assert.equal(queue.messages[0].actorId, actor.id.toString()) + assert.equal(queue.messages[0].toActorId, actor2.id.toString()) + assert.equal(queue.messages[1].activity.type, 'Delete') + assert.equal(queue.messages[1].actorId, actor.id.toString()) + assert.equal(queue.messages[1].toActorId, actor3.id.toString()) + }) }) }) diff --git a/functions/api/v1/statuses/[id].ts b/functions/api/v1/statuses/[id].ts index bc9bb4a..0819d01 100644 --- a/functions/api/v1/statuses/[id].ts +++ b/functions/api/v1/statuses/[id].ts @@ -1,6 +1,7 @@ // https://docs.joinmastodon.org/methods/statuses/#get import { type Note } from 'wildebeest/backend/src/activitypub/objects/note' +import * as activities from 'wildebeest/backend/src/activitypub/activities/delete' import { cors } from 'wildebeest/backend/src/utils/cors' import type { Person } from 'wildebeest/backend/src/activitypub/actors' import type { UUID } from 'wildebeest/backend/src/types' @@ -10,6 +11,8 @@ import type { Env } from 'wildebeest/backend/src/types/env' import * as errors from 'wildebeest/backend/src/errors' import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects' import { urlToHandle } from 'wildebeest/backend/src/utils/handle' +import { deliverFollowers } from 'wildebeest/backend/src/activitypub/deliver' +import type { Queue, DeliverMessageBody } from 'wildebeest/backend/src/types/queue' export const onRequestGet: PagesFunction = async ({ params, env, request }) => { const domain = new URL(request.url).hostname @@ -18,7 +21,7 @@ export const onRequestGet: PagesFunction = async ({ param export const onRequestDelete: PagesFunction = async ({ params, env, request, data }) => { const domain = new URL(request.url).hostname - return handleRequestDelete(env.DATABASE, params.id as UUID, data.connectedActor, domain) + return handleRequestDelete(env.DATABASE, params.id as UUID, data.connectedActor, domain, env.userKEK, env.QUEUE) } export async function handleRequestGet(db: D1Database, id: UUID, domain: string): Promise { @@ -62,7 +65,9 @@ export async function handleRequestDelete( db: D1Database, id: UUID, connectedActor: Person, - domain: string + domain: string, + userKEK: string, + queue: Queue ): Promise { const obj = (await getObjectByMastodonId(db, id)) as Note if (obj === null) { @@ -79,7 +84,9 @@ export async function handleRequestDelete( await deleteNote(db, obj) - // FIXME: deliver a Delete message to the Actor followers and our peers + // FIXME: deliver a Delete message to our peers + const activity = activities.create(domain, connectedActor, obj) + await deliverFollowers(db, userKEK, connectedActor, activity, queue) const headers = { ...cors(),