MOW-126: basic local status deletion

This change deletes the Note from local object and Actor's
outbox tables.
pull/205/head
Sven Sauleau 2023-01-31 17:02:42 +00:00
rodzic 3388675ad3
commit f0a6b695a6
2 zmienionych plików z 101 dodań i 10 usunięć

Wyświetl plik

@ -2,10 +2,9 @@ import { strict as assert } from 'node:assert/strict'
import { createReply } from 'wildebeest/backend/test/shared.utils'
import { createStatus, getMentions } from 'wildebeest/backend/src/mastodon/status'
import { createPublicNote, type Note } from 'wildebeest/backend/src/activitypub/objects/note'
import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects'
import { createImage } from 'wildebeest/backend/src/activitypub/objects/image'
import * as statuses from 'wildebeest/functions/api/v1/statuses'
import * as statuses_get from 'wildebeest/functions/api/v1/statuses/[id]'
import * as statuses_id from 'wildebeest/functions/api/v1/statuses/[id]'
import * as statuses_favourite from 'wildebeest/functions/api/v1/statuses/[id]/favourite'
import * as statuses_reblog from 'wildebeest/functions/api/v1/statuses/[id]/reblog'
import * as statuses_context from 'wildebeest/functions/api/v1/statuses/[id]/context'
@ -17,7 +16,7 @@ import * as activities from 'wildebeest/backend/src/activitypub/activities'
import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow'
import { MessageType } from 'wildebeest/backend/src/types/queue'
import { MastodonStatus } from 'wildebeest/backend/src/types'
import { mastodonIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
import { mastodonIdSymbol, getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects'
const userKEK = 'test_kek4'
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
@ -537,7 +536,7 @@ describe('Mastodon APIs', () => {
await insertLike(db, actor2, note)
await insertLike(db, actor3, note)
const res = await statuses_get.handleRequest(db, note[mastodonIdSymbol]!, domain)
const res = await statuses_id.handleRequestGet(db, note[mastodonIdSymbol]!, domain)
assert.equal(res.status, 200)
const data = await res.json<any>()
@ -553,7 +552,7 @@ describe('Mastodon APIs', () => {
const mediaAttachments = [await createImage(domain, db, actor, properties)]
const note = await createPublicNote(domain, db, 'my first status', actor, mediaAttachments)
const res = await statuses_get.handleRequest(db, note[mastodonIdSymbol]!, domain)
const res = await statuses_id.handleRequestGet(db, note[mastodonIdSymbol]!, domain)
assert.equal(res.status, 200)
const data = await res.json<any>()
@ -592,7 +591,7 @@ describe('Mastodon APIs', () => {
await insertReblog(db, actor2, note)
await insertReblog(db, actor3, note)
const res = await statuses_get.handleRequest(db, note[mastodonIdSymbol]!, domain)
const res = await statuses_id.handleRequestGet(db, note[mastodonIdSymbol]!, domain)
assert.equal(res.status, 200)
const data = await res.json<any>()
@ -810,5 +809,41 @@ describe('Mastodon APIs', () => {
const data = await res.json<{ error: string }>()
assert(data.error.includes('Limit exceeded'))
})
test('delete non-existing status', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const mastodonId = 'abcd'
const res = await statuses_id.handleRequestDelete(db, mastodonId, actor, domain)
assert.equal(res.status, 404)
})
test('delete status from a different actor', async () => {
const db = await makeDB()
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)
assert.equal(res.status, 404)
})
test('delete status', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const note = await createPublicNote(domain, db, 'note from actor', actor)
const res = await statuses_id.handleRequestDelete(db, note[mastodonIdSymbol]!, actor, domain)
assert.equal(res.status, 200)
{
const { count } = await db.prepare(`SELECT count(*) as count FROM outbox_objects`).first<any>()
assert.equal(count, 0)
}
{
const { count } = await db.prepare(`SELECT count(*) as count FROM objects`).first<any>()
assert.equal(count, 0)
}
})
})
})

Wyświetl plik

@ -1,17 +1,27 @@
// https://docs.joinmastodon.org/methods/statuses/#get
import { type Note } from 'wildebeest/backend/src/activitypub/objects/note'
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'
import type { ContextData } from 'wildebeest/backend/src/types/context'
import { getMastodonStatusById } from 'wildebeest/backend/src/mastodon/status'
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 { urlToHandle } from 'wildebeest/backend/src/utils/handle'
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ params, env, request }) => {
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ params, env, request }) => {
const domain = new URL(request.url).hostname
return handleRequest(env.DATABASE, params.id as UUID, domain)
return handleRequestGet(env.DATABASE, params.id as UUID, domain)
}
export async function handleRequest(db: D1Database, id: UUID, domain: string): Promise<Response> {
export const onRequestDelete: PagesFunction<Env, any, ContextData> = async ({ params, env, request, data }) => {
const domain = new URL(request.url).hostname
return handleRequestDelete(env.DATABASE, params.id as UUID, data.connectedActor, domain)
}
export async function handleRequestGet(db: D1Database, id: UUID, domain: string): Promise<Response> {
const status = await getMastodonStatusById(db, id, domain)
if (status === null) {
return new Response('', { status: 404 })
@ -23,3 +33,49 @@ export async function handleRequest(db: D1Database, id: UUID, domain: string): P
}
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 deleteOutboxObject = db.prepare('DELETE FROM outbox_objects WHERE id=?').bind(note.id.toString())
const deleteObject = db.prepare('DELETE FROM objects WHERE id=?').bind(note.id.toString())
const res = await db.batch([deleteOutboxObject, deleteObject])
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,
connectedActor: Person,
domain: string
): Promise<Response> {
const obj = (await getObjectByMastodonId(db, id)) as Note
if (obj === null) {
return errors.statusNotFound(id)
}
const status = await toMastodonStatusFromObject(db, obj, domain)
if (status === null) {
return errors.statusNotFound(id)
}
if (status.account.id !== urlToHandle(connectedActor.id)) {
return errors.statusNotFound(id)
}
await deleteNote(db, obj)
// FIXME: deliver a Delete message to the Actor followers and our peers
const headers = {
...cors(),
'content-type': 'application/json; charset=utf-8',
}
return new Response(JSON.stringify(status), { headers })
}