ππ» myapp47μμ μΆκ°λ μ¬ν μ
λλ€.
These are the additions made in myapp47.
βοΈ api/clenaup-orphan λΌμ°νΈλ‘ ν΄λΉκΈμ μ§μ μμ ν μ μμ΅λλ€.
You can delete the post directly using the api/cleanup-orphan route.
βοΈ app/users/[username]/statuses/[id]/route.ts μ΄ λΌμ°νΈμ tombstoneμ΄ μΆκ°λμμ΅λλ€.
A tombstone has been added to the app/users/[username]/statuses/[id]/route.ts route.
βοΈ app/api/v1/statuses/[id]/route.ts λΌμ°νΈκ° μΆκ°λμμ΅λλ€.(pinaforeμμ κΈ μμ ν λ μ¬μ© )
The app/api/v1/statuses/[id]/route.ts route has been added (used for deleting posts in Pinafore).
βοΈ μλμ κΈ°λ₯ ν
μ€νΈλ₯Ό μλ£νμ΅λλ€.
I have completed the functional tests below.
— ν
μ€νΈλ κ³ ν¬μμ
μλ²(freelifemakers.com)λ₯Ό κΈ°μ€μΌλ‘ ν
μ€νΈν©λλ€.
Testing is conducted based on the GoToSocial server (freelifemakers.com).
— λ‘컬μλ²μ κΈμ°κ³ 리λͺ¨νΈμλ²μ λ°°λ¬νμΈ
Write a post on the local server and verify delivery to the remote server.
— λ‘컬μλ²μ κΈ μμ μ 리λͺ¨νΈ μλ² μμ νμΈ
Verify deletion on the remote server when deleting a post on the local server.
— 리λͺ¨νΈ μλ²μ κΈ μ°κ³ λ‘컬μλ²λ‘ λ°°λ¬λλμ§ νμΈ
Write a post on the remote server and verify if it is delivered to the local server.
— 리λͺ¨νΈ μλ²μ κΈ μμ μ λ‘컬μλ² κΈλ μμ λλμ§ νμΈ
Check whether the local server post is also deleted when the remote server post is deleted.
— pinaforeμμ κΈ μμ μ λ‘컬μλ² κΈ μμ λλμ§ νμΈ
Check if the post is deleted from the local server when deleted in Pinafore.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
You can find the full code on GitHub.
https://github.com/gideonslife01/flm-nextjs
π μ 체 νλ‘μ νΈ κ΅¬μ‘° / Overall Project Structure
myapp project/
βββ app/ (Next.js App Router)
β βββ .well-known/webfinger/route.ts -> webfinger
β βββ api/follow/route.ts -> Follow API(temporary)
β βββ api/announce/route.ts -> Boost(Announcement)
β βββ api/like/route.ts. -> Like API
β βββ api/posts/route.ts -> Writing API
β βββ api/timeline/route.ts -> Timeline API
β βββ api/followinglist/route.ts -> followinglist API
β βββ api/followerslist/route.ts -> followerslist API
β βββ api/v1/instance/route.ts -> auth
β βββ api/v1/apps/route.ts. -> auth
β βββ api/v1/accounts/verify_credentials/route.ts -> auth
β βββ api/v1/statuses/route.ts -> Write Post
β βββ api/v1/timelines/home/route.ts -> timeline
β βββ oauth/authorize/route.ts -> auth
β βββ oauth/token/route.ts -> auth
β βββ api/v1/search/home/route.ts -> search
β βββ api/v2/search/home/route.ts -> search
β βββ api/v1/accounts/[id]/followers/route.ts -> followers
β βββ api/v1/accounts/[id]/following/route.ts -> following
β βββ api/v1/statuses/[id]/route.ts -> -> Post delete(pinafore)
β βββ api/auth/signup/route.ts -> Signup
β βββ api/auth/login/route.ts -> Login
β βββ api/auth/logout/route.ts -> Logout
β βββ api/auth/refresh/route.ts -> Refresh Token
β βββ api/cleanup-orphan/route.ts -> post,outbox clean up
β βββ api/auth/me/route.tsx -> Login Check
β βββ auth/signup/page.tsx -> Signup UI
β βββ auth/signup/page.tsx -> Login UI
β βββ users/[username]/
β β βββ statuses/[id]/route.ts -> Indivisual Post
β β βββ route.ts -> Acotr Information
β β βββ followers/route.ts -> Followers List
β β βββ following/route.ts -> Following List
β β βββ inbox/route.ts -> Inbox
β β βββ outbox/route.ts -> outbox
β βββ usersui/[username]/
β β βββ page.tsx -> Timeline UI(Server, 404)
β β βββ ClientPage.tsx. -> Timeline UI(Client,Call Active Themes)
β β βββ _components/themes/
β β βββ themeex/ThemeexTheme.tsx -> Example Theme
β β βββ pinafore/PinaforeTheme.tsx -> Theme 1
β β βββ pinafore/FollowersList.tsx -> Theme 1, FollowersList
β β βββ pinafore/FollowingList.tsx -> Theme 1, FollowingList
β β βββ pinafore/NotificationsList.tsx -> Theme 1, NotificationsList
β β βββ mastodon/MastodonTheme.tsx -> Theme 2
β β βββ minimal/MinimaltTheme.tsx -> Theme 3
β βββ layout.tsx, page.tsx, globals.css
β βββ favicon.ico
βββ lib/
β βββ theme.tsx -> Theme Provider
β βββ watchThemes.ts -> Check real-time theme changes
β βββ auth.ts -> Authentication, User Management
β βββ ap.ts -> Follow,Undo,Create,Likes,Announce
β βββ db.ts -> DB connection
βββ data/
β βββ keys/userIDs/ -> private.pem, public.pem(New)
β βββ keys/ -> private.pem, public.pem(legacy)
βββ data.sqlite -> Database(1/3)
βββ data.sqlite-wal -> Database(2/3)
βββ data.sqlite-shm -> Database(3/3)
βββ Caddyfile -> https
βββ instrumentation.ts -> Background Server
βββ package.json
π νλ‘μ νΈ μμ / Project Start
π cleanup-orphan λΌμ°νΈ μ¬μ©λ°©λ²
How to use the cleanup-orphan route
βοΈ μ¬μ©λ°©λ² / How to Use
# 1. μ 체 μμ μ 보νμΈ (GET)
# Check Bulk Deletion Information (GET)
Check Bulk Deletion Information (GET)
curl "https://aloy-horizon.duckdns.org/api/cleanup-orphan?username=user1"
# ν°λ―Έλμμ μ€ν μ / Example run in terminal
curl "https://aloy-horizon.duckdns.org/api/cleanup-orphan?id=5a24536b-06ec-400c-8539-4c1350dd1d19&username=user1"
# 2. μ λ ΉκΈλ§ μμ (postsμ outbox λΆμΌμΉ νλ κ²½μ°)
# Delete only "ghost posts" (cases where there is a mismatch between `posts` and `outbox`)
curl -X POST https://aloy-horizon.duckdns.org/api/cleanup-orphan \
-H "Content-Type: application/json"
-d '{"username":"user1"}'
# 3. 리λͺ¨νΈ(GoToSocial, Mastodon) + λ‘컬 μ 체 μμ
# Delete all remote (GoToSocial, Mastodon) + local data
curl -X DELETE "https://aloy-horizon.duckdns.org/api/cleanup-orphan?username=user1&all=true"
# 리λͺ¨νΈ + λ‘컬 κΈ μ§μ μμ / Directly delete remote + local posts
curl -X DELETE "https://aloy-horizon.duckdns.org/api/cleanup-orphan?id=5a24536b-06ec-400c-8539-4c1350dd1d19&username=user1"
# - ν°λ―Έλμμ μ€ν / Run in the terminal
curl -X DELETE "https://aloy-horizon.duckdns.org/api/cleanup-orphan?id=5a24536b-06ec-400c-8539-4c1350dd1d19&username=user1"
# - μλ΅ / Response
{"message":"β
5a24536b-06ec-400c-8539-4c1350dd1d19 리λͺ¨νΈ+λ‘컬 μμ μλ£! μ΄μ 410 Tombstone!","deletedId":"5a24536b-06ec-400c-8539-4c1350dd1d19","tombstoneUrl":"https://aloy-horizon.duckdns.org/users/user1/statuses/5a24536b-06ec-400c-8539-4c1350dd1d19","followersCount":2}
π μ½λ μμ / Code Modification
βοΈ app/users/[username]/inbox/route.ts
— 리λͺ¨νΈ μλ²(κ³ ν¬μμ
)μμ κΈμ μ°λ©΄ λ΄ μλ²μ λ°°λ¬λ©λλ€.
When you write a post on a remote server (GoToSocial), it is delivered to your server.
— 리λͺ¨νΈ μλ²μμ κΈ μμ μ λ΄ μλ²μ λ°°λ¬λ κΈλ μμ νλ κΈ°λ₯μ μΆκ°ν©λλ€.
Adds a feature to delete posts delivered to your server when they are deleted on the remote server.
... ...
// β
myapp47 - λ°°λ¬λ κΈ μμ / Delete delivered message
try {
// GoToSocial Deleteλ objectκ° string λλ object
// In GoToSocial Delete, the object is either a string or an object.
// object: "https://freelifemakers.com/users/user1/statuses/xxx"
let objectId = typeof body.object === 'string'? body.object : body.object?.id;
// TombstoneμΈ κ²½μ°
if (!objectId && typeof body.object === 'object') {
objectId = body.object?.id || body.object;
}
console.log(`π [${username}] Delete λμ°©! μμ ν κΈ: ${objectId}`);
if (objectId) {
const shortId = objectId.split('/').pop()?.split('#')[0]?.split('?')[0];
// β
myapp47
// 1. original_id μ νν μΌμΉ / Exact match for original_id
let result = db.prepare('DELETE FROM inbox_posts WHERE original_id=?').run(objectId);
// 2. μ§§μ IDλ‘ LIKE κ²μ (posts/ vs statuses/ νΈν + μλ ID νΈν)
// LIKE search using short IDs (compatible with posts/ vs. statuses/ and legacy IDs)
if (result.changes === 0 && shortId) {
result = db.prepare('DELETE FROM inbox_posts WHERE original_id LIKE?').run(`%${shortId}%`);
}
// 3. id 컬λΌμ shortIdλ‘ μ μ₯λ κ²½μ°
// When stored as a shortId in the id column
if (result.changes === 0 && shortId) {
result = db.prepare('DELETE FROM inbox_posts WHERE id=? OR id LIKE?').run(shortId, `%${shortId}%`);
}
// 4. content μμ IDκ° μλ κ²½μ° (GoToSocialμ΄ contentμ URL λ£μ΄λλ κ²½μ°)
// When there is an ID within the content (e.g., when GoToSocial embeds a URL in the content)
if (result.changes === 0 && shortId) {
try {
result = db.prepare('DELETE FROM inbox_posts WHERE content LIKE?').run(`%${shortId}%`);
} catch {}
}
console.log(`β
[${username}] inbox_posts μμ μλ£! changes=${result.changes} id=${shortId} objectId=${objectId}`);
}
} catch (e) {
console.error(`β Delete μ²λ¦¬ μ€ν¨ / Delete failed `, e);
}
return new Response('', { status: 202 });
... ...
βοΈ app/users/[username]/statuses/[id]/route.ts
— Tombstoneμ μΆκ° ν©λλ€.(μμ²μ΄ μλ μλ²λ₯Ό μν΄μ ꡬν)
Adds a tombstone (implemented for servers that requested it).
— λ΄ μλ² μλ³Έ κΈ μμ μ κΈμ΄ λ°°λ¬λ μλ²μ λ΄ κΈ μμ ν΄λ¬λΌλ DELETEμμ²μ 보λ
λλ€.
When the original post is deleted from my server, a DELETE request is sent to the servers where the post was delivered, asking them to delete it.
— 리λͺ¨νΈμλ²κ° DELETE μμ²μ λ°μΌλ©΄ λ΄ μλ²μμ app/users/[username]/statuses/[id] μ΄ λΌμ°νΈλ₯Ό μ€νν©λλ€.
When the remote server receives a DELETE request, it executes the app/users/[username]/statuses/[id] route on my server.
— μ΄ λΌμ°νΈλ μλ³ΈκΈμ λ΄ λ°μ΄ν°λ² μ΄μ€μμ κ²μν κΈμ΄ μλκ² νμΈλλ©΄ tombstone μΌλ‘ μλ΅ν©λλ€.
This route searches my database for the original post and responds with a tombstone if it confirms the post does not exist.
— tombstoneμ 리λͺ¨νΈ μλ²κ° νμΈνλ©΄ κΈμ μ€μ λ‘ μμ ν©λλ€.
When the remote server verifies the tombstone, it actually deletes the post.
— tombstoneμ νμΈ νλ μ΄μ λ νμ deleteμμ²μΌλ‘ κΈ μμ λλκ±Έ λ°©μ§ νκΈ° μν¨μ
λλ€.
The reason for checking the tombstone is to prevent posts from being deleted due to fraudulent delete requests.
... ...
export async function GET(
req: Request,
{ params }: { params: Promise<{ username: string; id: string }> }
) {
... ...
if (!post) {
// 404 λ§κ³ 410 + Tombstone, GoToSocial, Mastodonμ μμ νμΈ μ©
// For deletion confirmation on Tombstone, GoToSocial, Mastodon instead of 404 410
console.log(`πͺ¦ Tombstone μλ΅: ${id}`);
return NextResponse.json({
'@context': 'https://www.w3.org/ns/activitystreams',
id: `https://${DOMAIN}/users/${username}/statuses/${id}`,
type: 'Tombstone', // βοΈ
formerType: 'Note',
deleted: new Date().toISOString()
}, {
status: 410, // 410 Gone
headers: {
'Content-Type': 'application/activity+json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
}
});
}
// id, url λͺ¨λ /statuses/λ‘ μΌμΉ μν΄ / Match both id and url to /statuses/
const note = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `https://${DOMAIN}/users/${username}/statuses/${id}`,
type: 'Note',
attributedTo: `https://${DOMAIN}/users/${username}`,
content: post.content?.startsWith('<p>') ? post.content : `<p>${post.content}</p>`,
published: new Date(post.created_at || Date.now()).toISOString(),
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [`https://${DOMAIN}/users/${username}/followers`],
url: `https://${DOMAIN}/users/${username}/statuses/${id}`
};
... ...
}
... ...
π ν μ€νΈ / Test
βοΈ κ³ ν¬μμ
μμ κΈ μμ±μ λ‘컬μλ²λ‘ κΈ λ°°λ¬λλμ§ νμΈ
Verify if posts are delivered to the local server when creating a post on GoToSocial.


βοΈ λ¦¬λͺ¨νΈκΈ λ° λ‘컬 κΈ μ§μ μμ (cleanup-orphan)
Direct deletion of remote and local entries
— ν°λ―Έλμμ μ€ν / Run in the terminal
curl -X DELETE "https://aloy-horizon.duckdns.org/api/cleanup-orphan?id=5a24536b-06ec-400c-8539-4c1350dd1d19&username=user1"
–μλ²λ‘κ·Έ / Server Log

βοΈ λ‘컬μλ²μμ κΈ μμ±νκ³ μμ μ 리λͺ¨νΈ μλ² κΈλ μμ λλμ§ ν
μ€νΈ
Testing whether a post on the remote server is also deleted when creating and deleting a post on the local server.
— ν°λ―Έλμμ κΈμ°κΈ° / Writing in the terminal
— pinafore.socialμ λ‘κ·ΈμΈν΄μ κΈμ¨λ λ©λλ€.
You can log in to pinafore.social and write posts.
curl -X POST https://aloy-horizon.duckdns.org/api/posts \
-H "Content-Type: application/json" \
-d '{"username":"user1","content":"Hello local UI #apiposts myapp47"}'

βοΈ pinaforeμμ κΈ μμ κΈ°λ₯ ν
μ€νΈλ μλ£νκ³ μ΄λ―Έμ§ 첨λΆλ μ€λͺ
μ μλ΅ν©λλ€.
I have finished testing the post deletion function in Pinafore; I am omitting the image attachments and descriptions.
μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”