// Deleting your own post or reply.
//
// ⛔ A TOMBSTONE, like every other undo here (unfollow, unlike, unrepost) and like keet's own message
// delete: absence is ambiguous in a replicated log, so a peer has to be able to tell "deleted" from
// "not replicated to me yet". And the bridges need the same positive evidence to retract what they
// already published — bsky's record and the fediverse's Note are deleted only for a name that is
// KNOWN deleted, never for one that is merely missing from a listing.
//
// These drive `dispatch`, the thing the socket and the in-process client actually call.
import test from 'brittle'
import process from '#node:process'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import { FavelaNode } from '../lib/node.js'
import { dispatch } from '../lib/rpc.js'
import { saveCredential } from '../lib/device.js'
import { identity } from './helpers.js'
const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-postdel-'))
const stubTransport = () => ({ async join () {}, async leave () {}, async destroy () {} })
async function node () {
process.env.FAVELA_NAMESPACE = 'own'
const storage = tmp()
const id = await identity()
const cred = await id.device()
saveCredential(storage, cred)
const n = new FavelaNode(storage, { transport: stubTransport() })
await n.ready()
await n.link(cred)
return n
}
test('★ deleting my post removes it from my posts and records it as deleted', async (t) => {
const n = await node()
t.teardown(() => n.close())
const { feed } = await dispatch(n, 'whoami', {})
const keep = await dispatch(n, 'post', { body: 'keep me' })
const oops = await dispatch(n, 'post', { body: 'posted twice by accident' })
const before = await dispatch(n, 'posts-by', { feed })
t.ok(before.length === 2 && before.every((p) => p.own === true), 'my rows are marked own — what offers the delete button')
const r = await dispatch(n, 'post-delete', { name: oops.name })
t.is(r.deleted, oops.name, 'it says what it deleted')
t.is(r.collection, 'post')
const names = (await dispatch(n, 'posts-by', { feed })).map((p) => p.name)
t.ok(names.includes(keep.name), 'the other post is untouched')
t.absent(names.includes(oops.name), 'the deleted one is gone from my posts')
const gone = await dispatch(n, 'deleted-own', {})
t.alike(gone.post, [oops.name], 'and is listed as POSITIVELY deleted — what a bridge retracts on')
t.alike(gone.reply, [], 'no replies deleted')
})
test('deleting my reply works the same way', async (t) => {
const n = await node()
t.teardown(() => n.close())
const { feed } = await dispatch(n, 'whoami', {})
const parent = await dispatch(n, 'post', { body: 'parent' })
const rep = await dispatch(n, 'reply', { target: { feed, collection: 'post', name: parent.name }, body: 'a reply' })
const r = await dispatch(n, 'post-delete', { name: rep.name })
t.is(r.collection, 'reply', 'it found the record among my replies')
t.alike((await dispatch(n, 'deleted-own', {})).reply, [rep.name])
const replies = await dispatch(n, 'replies', { target: { feed, collection: 'post', name: parent.name } })
t.absent(replies.some((x) => x.name === rep.name), 'and it is gone from the thread')
})
test('a name I never wrote is refused, and deleting twice is not an error', async (t) => {
const n = await node()
t.teardown(() => n.close())
await t.exception(dispatch(n, 'post-delete', { name: 'not-a-record-of-mine' }), /no post or reply/, 'nothing to delete is said, not ignored')
const p = await dispatch(n, 'post', { body: 'once' })
await dispatch(n, 'post-delete', { name: p.name })
const again = await dispatch(n, 'post-delete', { name: p.name })
t.is(again.deleted, p.name, 'a second press finds it already deleted')
t.ok(again.already, 'and says so')
t.alike((await dispatch(n, 'deleted-own', {})).post, [p.name], 'still one tombstone')
})
test('a holder of the feed can read the tombstones too — what ap-bridge retracts from', async (t) => {
const n = await node()
t.teardown(() => n.close())
const p = await dispatch(n, 'post', { body: 'gone soon' })
await dispatch(n, 'post-delete', { name: p.name })
t.alike(await n.peer.feed.tombstones('post'), [p.name], 'Feed.tombstones(collection) lists deleted names')
})