MOW-139: support federated Note deletion

pull/220/head
Sven Sauleau 2023-02-08 17:30:00 +00:00
rodzic c96a748e4b
commit 13b827dcfa
4 zmienionych plików z 111 dodań i 28 usunięć

Wyświetl plik

@ -24,7 +24,7 @@ import { insertLike } from 'wildebeest/backend/src/mastodon/like'
import { createReblog } from 'wildebeest/backend/src/mastodon/reblog'
import { insertReply } from 'wildebeest/backend/src/mastodon/reply'
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
import { originalActorIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
import { originalActorIdSymbol, deleteObject } from 'wildebeest/backend/src/activitypub/objects'
import { hasReblog } from 'wildebeest/backend/src/mastodon/reblog'
function extractID(domain: string, s: string | URL): string {
@ -342,6 +342,25 @@ export async function handle(
break
}
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete
case 'Delete': {
const objectId = getObjectAsId()
const obj = await objects.getObjectById(db, objectId)
if (obj === null) {
console.warn('unknown object')
break
}
if (!['Note'].includes(obj.type)) {
console.warn('unsupported Update for Object type: ' + activity.object.type)
return
}
await deleteObject(db, obj)
break
}
default:
console.warn(`Unsupported activity: ${activity.type}`)
}

Wyświetl plik

@ -272,3 +272,28 @@ function getNameRewriter() {
})
return nameRewriter
}
// TODO: eventually use SQLite's `ON DELETE CASCADE` but requires writing the DB
// schema directly into D1, which D1 disallows at the moment.
// Some context at: https://stackoverflow.com/questions/13150075/add-on-delete-cascade-behavior-to-an-sqlite3-table-after-it-has-been-created
export async function deleteObject<T extends APObject>(db: D1Database, note: T) {
const nodeId = note.id.toString()
const batch = [
db.prepare('DELETE FROM outbox_objects WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM inbox_objects WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_notifications WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_favourites WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_reblogs WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_replies WHERE object_id=?1 OR in_reply_to_object_id=?1').bind(nodeId),
db.prepare('DELETE FROM idempotency_keys WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM objects WHERE id=?').bind(nodeId),
]
const res = await db.batch(batch)
for (let i = 0, len = res.length; i < len; i++) {
if (!res[i].success) {
throw new Error('SQL error: ' + res[i].error)
}
}
}

Wyświetl plik

@ -593,5 +593,69 @@ describe('ActivityPub', () => {
assert.equal(count, 1)
})
})
describe('Delete', () => {
test('delete Note', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Delete',
actor: actorA.id,
to: [],
cc: [],
object: note.id,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const { count } = await db.prepare('SELECT count(*) as count FROM objects').first<{ count: number }>()
assert.equal(count, 0)
})
test('delete Tombstone', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Delete',
actor: actorA.id,
to: [],
cc: [],
object: {
type: 'Tombstone',
id: note.id,
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const { count } = await db.prepare('SELECT count(*) as count FROM objects').first<{ count: number }>()
assert.equal(count, 0)
})
test('delete Actor', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const activity: any = {
type: 'Delete',
actor: actorA.id,
to: [],
cc: [],
object: actorA.id,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
// Ensure that we didn't actually delete the actor
const { count } = await db.prepare('SELECT count(*) as count FROM actors').first<{ count: number }>()
assert.equal(count, 1)
})
})
})
})

Wyświetl plik

@ -10,7 +10,7 @@ import type { ContextData } from 'wildebeest/backend/src/types/context'
import { getMastodonStatusById, toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status'
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 { getObjectByMastodonId, deleteObject } 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'
@ -48,31 +48,6 @@ export async function handleRequestGet(db: D1Database, id: UUID, domain: string)
return new Response(JSON.stringify(status), { headers })
}
// TODO: eventually use SQLite's `ON DELETE CASCADE` but requires writing the DB
// schema directly into D1, which D1 disallows at the moment.
// Some context at: https://stackoverflow.com/questions/13150075/add-on-delete-cascade-behavior-to-an-sqlite3-table-after-it-has-been-created
async function deleteNote(db: D1Database, note: Note) {
const nodeId = note.id.toString()
const batch = [
db.prepare('DELETE FROM outbox_objects WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM inbox_objects WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_notifications WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_favourites WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_reblogs WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM actor_replies WHERE object_id=?1 OR in_reply_to_object_id=?1').bind(nodeId),
db.prepare('DELETE FROM idempotency_keys WHERE object_id=?').bind(nodeId),
db.prepare('DELETE FROM objects WHERE id=?').bind(nodeId),
]
const res = await db.batch(batch)
for (let i = 0, len = res.length; i < len; i++) {
if (!res[i].success) {
throw new Error('SQL error: ' + res[i].error)
}
}
}
export async function handleRequestDelete(
db: D1Database,
id: UUID,
@ -95,7 +70,7 @@ export async function handleRequestDelete(
return errors.statusNotFound(id)
}
await deleteNote(db, obj)
await deleteObject(db, obj)
// FIXME: deliver a Delete message to our peers
const activity = activities.create(domain, connectedActor, obj)