kopia lustrzana https://github.com/cloudflare/wildebeest
MOW-139: support federated Note deletion
rodzic
c96a748e4b
commit
13b827dcfa
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue