[nextjs]SNS Server-37(myapp47)Β 

πŸ‘‰πŸ» 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.

gotosocial
aloy-horizon.duckdns.org

βœ”οΈ 리λͺ¨νŠΈκΈ€ 및 둜컬 κΈ€ μ§μ ‘μ‚­μ œ(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

cleanup-orphan – 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"}'
Server Log

βœ”οΈ 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”

Leave a Reply