[nextjs]SNS Server-7(myapp17)

๐Ÿ‘‰๐Ÿป ์ด๋ฒˆ ํฌ์ŠคํŠธ๋Š” ํŒ”๋กœ์ž‰ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.
This post covers the implementation of the following list feature.

๐Ÿ‘‰๐Ÿป ํŒ”๋กœ์ž‰๊ณผ ํŒ”๋กœ์šฐ ๊ฐœ๋…์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
The concepts of “following” and “being followed” are as follows.

1. followers

- ๋‚จ(user1@freelifemakers.com)์ด ๋‚˜๋ฅผ ํŒ”๋กœ์šฐํ•˜๋Š” ๊ฒฝ์šฐ ์ž…๋‹ˆ๋‹ค.
This is the case where someone (user1@freelifemakers.com) follows me.

- ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ณผ์ •์ด ์ˆ˜ํ–‰๋˜๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.
This applies when the following process is carried out.

  1)pinafore.social์— ๋กœ๊ทธ์ธ(user1@freelifemakers.com)
   Log in to pinafore.social (user1@freelifemakers.com)

  2)user1@aloy-horizon.duckdns.org์„ ๊ฒ€์ƒ‰
   Search for user1@aloy-horizon.duckdns.org

  3)ํŒ”๋กœ์šฐ ๋ฒ„ํŠผ ํด๋ฆญํ•˜๊ธฐ
   Click the Follow button.

  4)๋‚˜์˜ ์„œ๋ฒ„ inbox๋กœ Follow ๋“ค์–ด์˜ค๊ณ  followers ํ…Œ์ด๋ธ”์— ์ €์žฅ
   A "follow" request arrives in my server's inbox and is stored in the `followers` table.

2. following 

- ๋‚ด(user1@aloy-horizon.duckdns.org)๊ฐ€ ๋‚จ(user1@freelifemakers.com)์„ ํŒ”๋กœ์šฐํ•˜๋Š” ๊ฒฝ์šฐ ์ž…๋‹ˆ๋‹ค.
This is the case where I (user1@aloy-horizon.duckdns.org) follow another person (user1@freelifemakers.com).

- ์ด๊ฑด pinafore์—์„œ ํ…Œ์ŠคํŠธ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
This is not tested in Pinafore.

- ๋‚ด ์„œ๋ฒ„๊ฐ€ user1@freelifemakers.com๊ฐ™์€ ์™ธ๋ถ€ ์‚ฌ๋žŒ์„ ํŒ”๋กœ์šฐํ•˜๋Š” ๊ฒฝ์šฐ ์ž…๋‹ˆ๋‹ค.
This is the case where my server follows an external user, such as user1@freelifemakers.com.

- ๋‚ด๊ฐ€ํŒ”๋กœ์šฐ ํ•˜๋Š” ์‚ฌ๋žŒ์€ following ํ…Œ์ด๋ธ”์— ์ €์žฅ ๋ฉ๋‹ˆ๋‹ค.
The people I follow are stored in the 'following' table.

๐Ÿ“ ํ”„๋กœ์ ํŠธ ์„ค์น˜,๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜,HTTPS์„ค์ •
Project setup, library installation, HTTPS configuration

โœ”๏ธ ์•„๋ž˜์˜ ์ด์ „ ํฌ์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”
Please refer to the previous post below.

๐Ÿ“ ์ „์ฒด ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ / Overall Project Structure

myapp16/  
โ”œโ”€โ”€ app/  (Next.js App Router)
โ”‚   โ”œโ”€โ”€ .well-known/webfinger/route.ts  -> webfinger
โ”‚   โ”œโ”€โ”€ api/posts/route.ts     -> ๊ธ€ ์“ฐ๊ธฐ API / Writing API
โ”‚   โ”œโ”€โ”€ api/follow/route.ts    -> ํŒ”๋กœ์šฐ API(์ž„์‹œ) / Follow API(temporary)
โ”‚   โ”œโ”€โ”€ users/[username]/
โ”‚   โ”‚   โ”œโ”€โ”€ route.ts           -> Actor์ •๋ณด / Acotr Information
โ”‚   โ”‚   โ”œโ”€โ”€ followers/route.ts -> Followers List
โ”‚   โ”‚   โ”œโ”€โ”€ folloing/route.ts  -> Following List
โ”‚   โ”‚   โ”œโ”€โ”€ inbox/route.ts     -> Inbox
โ”‚   โ”‚   โ””โ”€โ”€ outbox/route.ts    -> outbox
โ”‚   โ”œโ”€โ”€ layout.tsx, page.tsx, globals.css
โ”‚   โ””โ”€โ”€ favicon.ico
โ”œโ”€โ”€ lib/
โ”‚   โ”œโ”€โ”€ ap.ts                  -> Follow Accept
โ”‚   โ””โ”€โ”€ db.ts                  -> DB connection
โ”œโ”€โ”€ data/
โ”‚   โ””โ”€โ”€ keys/                  -> private.pem, public.pem
โ”œโ”€โ”€ data.sqlite                -> Database
โ”œโ”€โ”€ Caddyfile                  -> https 
โ””โ”€โ”€ package.json

๐Ÿ“ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘(myapp17)
Project Start (myapp17)

npx create-next-app@latest

๐Ÿ“ SQLite์„ค์น˜ / Installing SQLite

cd ~/myapp17
npm install better-sqlite3
npm install -D @types/better-sqlite3

๐Ÿ“ following ํ…Œ์ด๋ธ” ๋ฐ ์ธ๋ฑ์Šค ์ƒ์„ฑ

CREATE TABLE following (
  id TEXT PRIMARY KEY,
  actor TEXT NOT NULL,
  username TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_following_username ON following(username);
CREATE INDEX idx_following_actor ON following(actor);

๐Ÿ“ ์ฝ”๋“œ ์ˆ˜์ • ๋ฐ ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Code modifications and route additions

โœ”๏ธ ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add Route

— app/users/[username]/following/route.ts 

import db from '@/lib/db';

export async function GET(
  req: Request, 
  { params }: { params: Promise<{ username: string }> }
) {
  const { username } = await params; // โ† await ํ•ด์•ผํ•จ / must await
  const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

  let following: any[] = [];
  try {
    following = db.prepare('SELECT actor FROM following WHERE username = ?').all(username) as any[];
  } catch (e) {
    console.log('following ํ…Œ์ด๋ธ” ์—†์Œ ๋˜๋Š” ์—๋Ÿฌ / Following table not found or error:', e);
  }

  return Response.json({
    "@context": "https://www.w3.org/ns/activitystreams",
    "id": `https://${DOMAIN}/users/${username}/following`,
    "type": "OrderedCollection",
    "totalItems": following.length,
    "orderedItems": following.map((f: any) => f.actor)
  }, {
    headers: {
      'Content-Type': 'application/activity+json; charset=utf-8',
      'Access-Control-Allow-Origin': '*'
    }
  });
}

1) ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐํ•œ ์‚ฌ์šฉ์ž ๋ฆฌ์ŠคํŠธ๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
Displays the list of users I follow.

โœ”๏ธ ์ฝ”๋“œ ์ˆ˜์ • / Code modification

— app/api/follow/route.ts

import { sendFollow, signedFetch } from '@/lib/ap';
import db from '@/lib/db'; 

// ํŒ”๋กœ์šฐ API / Follow API
export async function POST(req: Request) {

... ...

    // myapp17โœ… 
    //  4.์„ฑ๊ณตํ•˜๋ฉด following ํ…Œ์ด๋ธ”์— ์ €์žฅ! / If successful, save to the following table!
    if (result.ok) {
      try {
        db.prepare('INSERT OR IGNORE INTO following (id, actor, username) VALUES (?, ?, ?)')
          .run(result.followDoc.id, target, username);
        console.log(`โœ… following DB ์ €์žฅ / If successful, save to the following table!: ${username} -> ${target}`);
      } catch (e) {
        console.error('DB ์ €์žฅ ์‹คํŒจ / DB save failed:', e);
      }
    }

... ...

} 

// ์–ธํŒ”๋กœ์šฐ ์ถ”๊ฐ€ / Unfollow added
export async function DELETE(req: Request) {
  try {
    const { username, target } = await req.json();
    if (!username || !target) return Response.json({ error: 'username, target ํ•„์š” / Username and target required' }, { status: 400 });

    const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
    
    // Undo Follow ๋งŒ๋“ค๊ธฐ (sendFollow ์ฐธ๊ณ ํ•ด์„œ) / Create Undo Follow (refer to sendFollow)
    const { sendUndoFollow } = await import('@/lib/ap');
    const result = await sendUndoFollow(target, username);

    // DB์—์„œ ์‚ญ์ œ / Delete from DB
    db.prepare('DELETE FROM following WHERE actor = ? AND username = ?').run(target, username);
    console.log(`๐Ÿ—‘๏ธ following ์‚ญ์ œ / Delete from DB: ${username} -X-> ${target}`);

    return Response.json({ ok: true, result });
  } catch (e: any) {
    return Response.json({ error: e.message }, { status: 500 });
  }
}

1)ํŒ”๋กœ์šฐ,์–ธํŒ”๋กœ์šฐ ํ• ๋•Œ followingํ…Œ์ด๋ธ”์—์„œ ์‚ฌ์šฉ์ž๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์‚ญ์ œ ํ•ฉ๋‹ˆ๋‹ค.
Users are added to and removed from the following table when following or unfollowing.

— lib/ap.ts

port async function sendUndoFollow(targetActor: string, username: string) {
  const actorId = `https://${DOMAIN}/users/${username}`;

  // let์œผ๋กœ ๋ฐ–์— ์„ ์–ธ(ํƒ€์ž…์—๋Ÿฌ ์ˆ˜์ •) / Declare outside with let (type error fix)
  let followId = `${actorId}/follows/${targetActor}`; 

  try {
    const db = (await import('@/lib/db')).default;
    const row = db.prepare('SELECT id FROM following WHERE actor = ? AND username = ?').get(targetActor, username) as any;
    if (row?.id) {
      followId = row.id;
    }
  } catch {}

  const actorRes = await signedFetch(targetActor, username);
  if (!actorRes.ok) {
    const t = await actorRes.text();
    throw new Error(`๋Œ€์ƒ ์กฐํšŒ ์‹คํŒจ ${actorRes.status}: ${t}`);
  }
  const actorData = await actorRes.json();
  const inbox = actorData.inbox;

  const undoId = `${actorId}#undo/${Date.now()}`;
  const undoDoc = {
    '@context': 'https://www.w3.org/ns/activitystreams',
    id: undoId,
    type: 'Undo',
    actor: actorId,
    object: {
      id: followId,
      type: 'Follow',
      actor: actorId,
      object: targetActor
    }
  };

  const body = JSON.stringify(undoDoc);
  const url = new URL(inbox);
  const digest = `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
  const date = new Date().toUTCString();
  const signingString = `(request-target): post ${url.pathname}\nhost: ${url.host}\ndate: ${date}\ndigest: ${digest}`;
  
  const signer = crypto.createSign('sha256');
  signer.update(signingString);
  const signature = signer.sign(PRIVATE_KEY, 'base64');

  const keyId = `${actorId}#main-key`;
  const sigHeader = `keyId="${keyId}",headers="(request-target) host date digest",signature="${signature}"`;

  console.log(`โ†ฉ๏ธ [${username}] Undo Follow ์ „์†ก -> ${inbox}`);

  const res = await fetch(inbox, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/activity+json',
      'Date': date,
      'Digest': digest,
      'Signature': sigHeader,
      'Host': url.host
    },
    body
  });

  const text = await res.text();
  console.log(`๐Ÿ“ฌ Undo ๊ฒฐ๊ณผ / Undo result: ${res.status}`, text);
  return { ok: res.ok, status: res.status, text, undoDoc };
}

1)app/api/follow/route.ts์—์„œ ํ˜ธ์ถœ๋˜๋Š” ํŒ”๋กœ์šฐ ์ทจ์†Œ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
This is the unfollow function called from app/api/follow/route.ts.

๐Ÿ“ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ / Run a test

โœ”๏ธ ๋ธŒ๋ผ์šฐ์ €์—์„œ folllowing ๋ฆฌ์ŠคํŠธ ๋ณด๊ธฐ
View the following list in the browser.

https://aloy-horizon.duckdns.org/users/user1/following
following

โœ”๏ธ ํ„ฐ๋ฏธ๋„์—์„œ ํŒ”๋กœ์šฐ,์–ธํŒ”๋กœ์šฐ ,ํŒ”๋กœ์ž‰ ๋ฆฌ์ŠคํŠธ๋ณด๊ธฐ
Follow, unfollow, and view following list in the terminal.

# 1. user1์ด freelifemakers๋ฅผ ํŒ”๋กœ์šฐ (following์— ์ €์žฅ)
# user1 follows freelifemakers (saved to 'following')

curl -X POST https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1", "target":"https://freelifemakers.com/users/user1"}'

# ํ™•์ธ / check
curl https://aloy-horizon.duckdns.org/users/user1/following
# โ†’ totalItems: 1

# 2. ์–ธํŒ”๋กœ์šฐ (following์—์„œ ์‚ญ์ œ)
# Unfollow (remove from 'following' list)

curl -X DELETE https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1", "target":"https://freelifemakers.com/users/user1"}'

# ํ™•์ธ / check
curl https://aloy-horizon.duckdns.org/users/user1/following
# โ†’ totalItems: 0

โœ”๏ธ ํ„ฐ๋ฏธ๋„์—์„œ ์‹คํ–‰๊ฒฐ๊ณผ (ํŒ”๋กœ์šฐ)
Execution results in the terminal (follow)

following % curl -X POST https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1", "target":"https://freelifemakers.com/users/user1"}'
{"ok":true,"inbox":"https://freelifemakers.com/users/user1/inbox","target":"https://freelifemakers.com/users/user1","username":"user1","result":"{\"status\":\"Accepted\"}","follow":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/follows/1787190758589","type":"Follow","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1"}}%                                                          
following % 

๐Ÿ“ ์ง€๊ธˆ๊นŒ์ง€ ์™„๋ฃŒํ•œ ๋‚ด์šฉ / Work completed so far

— WebFinger  (users ํ…Œ์ด๋ธ” ์ƒ์„ฑ + DB ๊ฒ€์ƒ‰)
WebFinger (create ‘users’ table + DB search)

— Follow ๋ณด๋‚ด๊ธฐ / ๋ฐ›๊ธฐ
Follow Send/Receive

— ๊ธ€ ๋ฐฐ๋‹ฌ
Article(Post) Delivery

— ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐํ•œ ๋ฆฌ์ŠคํŠธ ๋งŒ๋“ค๊ธฐ
Create a list of people I follow

— ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ผ์šฐํŠธ
Routes that can be tested in a browser

1. WebFinger
https://aloy-horizon.duckdns.org/.well-known/webfinger?resource=acct:user1@aloy-horizon.duckdns.org

2. Actor (ํ”„๋กœํ•„ / Profile)
https://aloy-horizon.duckdns.org/users/user1

3. Followers (๋‚จ์ด ๋‚˜ ํŒ”๋กœ์šฐ / Someone follows me)
https://aloy-horizon.duckdns.org/users/user1/followers

4. Following (๋‚ด๊ฐ€ ๋‚จ์„ ํŒ”๋กœ์šฐ / I follow others)
https://aloy-horizon.duckdns.org/users/user1/following

5. Outbox (๋‚ด ๊ธ€ / My Post)
https://aloy-horizon.duckdns.org/users/user1/outbox

6. ๊ฐœ๋ณ„ ๊ธ€ / indivisual posts 
myapp18์—์„œ ๊ตฌํ˜„ ์˜ˆ์ •
Scheduled for implementation in myapp18.

— ํ„ฐ๋ฏธ๋„์—์„œ๋งŒ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๋ผ์šฐํŠธ
Routes that can only be tested in the terminal

# ํŒ”๋กœ์šฐ (following 1๋กœ ์ฆ๊ฐ€)
# Follow (following increased to 1)

curl -X POST https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://freelifemakers.com/users/user1"}'

# ์–ธํŒ”๋กœ์šฐ (following 0์œผ๋กœ ๊ฐ์†Œ)
# Unfollow (following count reduced to 0)

curl -X DELETE https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://freelifemakers.com/users/user1"}'

# ๊ธ€ ์“ฐ๊ธฐ (outbox 7๋กœ ์ฆ๊ฐ€ + ํŒ”๋กœ์›Œ๋“ค์—๊ฒŒ ๋ฐฐ๋‹ฌ)
# Writing a post (added to outbox 7 + delivered to followers)

curl -X POST https://aloy-horizon.duckdns.org/api/posts \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","content":"Hello Fediverse! #test"}'

1)๋ธŒ๋ผ์šฐ์ €์—์„œ ์‚ฌ์šฉ์‹œ POST์ „์†ก์ด๋‚˜ DELETE์ „์†ก์€ ํผ submit๋“ฑ์˜ ํ˜•์‹์œผ๋กœ ์ „์†ก๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
When using a browser, POST or DELETE requests must be sent using a format such as a “form submit.”


๐Ÿ‘‰๐Ÿป ์—ฌ๊ธฐ์„œ๋Š” ๋‚ด๊ฐ€ ์“ด๊ธ€์€ postsํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๊ณ  ๋‚ด๊ฐ€ ํŒ”๋กœ์ž‰ํ•˜๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ›์€ ๊ธ€์€ inbox_posts์— ์ €์žฅ๋˜๋„๋ก ์ˆ˜์ •ํ•˜๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.
Here, we are modifying the system so that posts I write are stored in the posts table, while posts received from servers I follow are stored in inbox_posts.

๐Ÿ‘‰๐Ÿป postํ…Œ์ด๋ธ”์— ์™ธ๋ถ€ ๊ธ€์ด ์„ž์ด๋Š” ๋ฒ„๊ทธ๋ฅผ ์ˆ˜์ •ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.
This change fixes a bug where external posts were getting mixed into the post table.

๐Ÿ“ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ / DB Migration

sqlite3 data.db

# posts์— username ์ปฌ๋Ÿผ ์ถ”๊ฐ€
# Add username column to posts
ALTER TABLE posts ADD COLUMN username TEXT DEFAULT 'user1';

# ์ž˜๋ชป ๋“ค์–ด๊ฐ„ remote ๊ธ€ ์‚ญ์ œ
# Deleted accidentally posted remote-related entry
DELETE FROM posts WHERE id LIKE 'https://%';

# inbox_posts ํ…Œ์ด๋ธ” ๋งŒ๋“ค๊ธฐ
# Create the inbox_posts table
CREATE TABLE IF NOT EXISTS inbox_posts (
  id TEXT PRIMARY KEY,
  actor TEXT,
  content TEXT,
  username TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

๐Ÿ“ ์ฝ”๋“œ ์ˆ˜์ • / Code Modification

โœ”๏ธ api/posts/route.ts

// app/api/posts/route.ts

// โœ… myapp17
import db from '@/lib/db';
import { randomUUID } from 'crypto';
import { sendNote } from '@/lib/ap';

const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const username = searchParams.get('username') || 'user1';

  // โœ… ํ•ด๋‹น ์œ ์ € ๊ธ€๋งŒ / Only that user's posts
  const posts = db.prepare('SELECT * FROM posts WHERE username = ? ORDER BY created_at DESC').all(username);
  return Response.json(posts);
}

export async function POST(req: Request) {
  const { content, username = 'user1' } = await req.json();
  if (!content) return Response.json({ error: '๋‚ด์šฉ ์—†์Œ / Content required' }, { status: 400 });

  const id = randomUUID();
  
  // โœ… username ์ €์žฅ! / Save username!
  db.prepare('INSERT INTO posts (id, content, username) VALUES (?, ?, ?)').run(id, content, username);

  const noteId = `https://${DOMAIN}/users/${username}/posts/${id}`;
  const note = {
    id: noteId,
    type: 'Note',
    attributedTo: `https://${DOMAIN}/users/${username}`,
    content: content,
    to: ['https://www.w3.org/ns/activitystreams#Public'],
    cc: [`https://${DOMAIN}/users/${username}/followers`]
  };

  // โœ… ํ•ด๋‹น ์œ ์ €์˜ ํŒ”๋กœ์›Œ๋งŒ!
  const followers = db.prepare('SELECT * FROM followers WHERE username = ?').all(username) as any[];
  console.log(`๐Ÿ“ค [${username}] ${followers.length}๋ช…์—๊ฒŒ ๋ฐฐ๋‹ฌ`);

  for (const follower of followers) {
    try {
      await sendNote(follower.inbox, note, username, id, content);
      console.log(`โœ… ๋ฐฐ๋‹ฌ ์„ฑ๊ณต / Delivery successful -> ${follower.actor}`);
    } catch (e) {
      console.error(`โŒ ๋ฐฐ๋‹ฌ ์‹คํŒจ / Delivery failed -> ${follower.actor}`, e);
    }
  }

  const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);
  return Response.json(post);
}

export async function PUT(req: Request) {
  const { id, content, username = 'user1' } = await req.json();
  if (!id || !content) {
    return Response.json({ error: 'id์™€ content ํ•„์š”' }, { status: 400 });
  }
  // โœ… ๋‚ด ๊ธ€๋งŒ ์ˆ˜์ • / Only edit my own posts
  const result = db.prepare('UPDATE posts SET content = ? WHERE id = ? AND username = ?').run(content, id, username);
  if (result.changes === 0) {
    return Response.json({ error: 'ํ•ด๋‹น ๊ธ€ ์—†์Œ / Post not found' }, { status: 404 });
  }
  const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);
  return Response.json(post);
}

export async function DELETE(req: Request) {
  const { searchParams } = new URL(req.url);
  const id = searchParams.get('id');
  const username = searchParams.get('username') || 'user1';
  if (!id) return Response.json({ error: 'id ํ•„์š” / id required' }, { status: 400 });
  // โœ… ๋‚ด ๊ธ€๋งŒ ์‚ญ์ œ / Only delete my own posts
  db.prepare('DELETE FROM posts WHERE id = ? AND username = ?').run(id, username);
  return Response.json({ ok: true });
}

โœ”๏ธ app/inbox/route.ts

    if (body.type === 'Create') {
      const note = body.object;
      if (note && note.type === 'Note') {
        console.log(`๐Ÿ“ [${username}] ์ƒˆ ๊ธ€ ๋„์ฐฉ / New post received from ${actorId}`);
        console.log(`๋‚ด์šฉ/content: ${note.content?.slice(0, 100)}`);

        try {
          const postId = note.id || `remote-${Date.now()}-${Math.random()}`;
          const content = note.content || '';

          // author ์ปฌ๋Ÿผ ์—†์œผ๋ฉด ์—๋Ÿฌ๋‚˜๋‹ˆ๊นŒ content์— ์ž‘์„ฑ์ž ํฌํ•จํ•ด์„œ ์ €์žฅ
          // If there's no author column, include the author in the content to avoid errors
          const fullContent = `[from: ${actorId}] ${content}`;

          // db.prepare('INSERT OR IGNORE INTO posts (id, content) VALUES (?,?)')
          //  .run(postId, fullContent);
          //console.log(` [${username}] ๊ธ€ ์ €์žฅ ์™„๋ฃŒ / Post saved successfully : ${postId}`);

           // โœ… myapp17 - inbox_posts์— ์ €์žฅ! posts ์•„๋‹˜! / Save to inbox_posts, not posts!
          db.prepare(`
            INSERT OR IGNORE INTO inbox_posts (id, actor, content, username) 
            VALUES (?, ?, ?, ?)
          `).run(postId, actorId, content, username);

          console.log(`โœ… [${username}] inbox_posts ์ €์žฅ ์™„๋ฃŒ : ${postId}`);

        } catch (e) {
          console.error(`โŒ ๊ธ€ ์ €์žฅ ์‹คํŒจ / Failed to save post`, e);
        }
      }
      return new Response('', { status: 202 });
    }

๐Ÿ“ ํ…Œ์ŠคํŠธ / Test

โœ”๏ธ ๋‚ด ์„œ๋ฒ„์— ๊ธ€ ์“ฐ๊ธฐ / write post to my server

# ๊ธ€ ์“ฐ๊ธฐ (outbox 7๋กœ ์ฆ๊ฐ€ + ํŒ”๋กœ์›Œ๋“ค์—๊ฒŒ ๋ฐฐ๋‹ฌ)
# Writing a post (added to outbox 7 + delivered to followers)

curl -X POST https://aloy-horizon.duckdns.org/api/posts \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","content":"Hello Fediverse! #test"}'
myapp17 % sqlite3 data.sqlite
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.
sqlite> select * from posts;
e6c99652-572f-491a-9e61-05e67b0d58fc|Hello! my SNS!!|2026-08-12 04:59:51|user1
dcece9ee-8d4a-4f63-91e1-22fc60513f62|Hello! my SNS!!|2026-08-12 05:00:18|user1
cc30330e-7040-4a1b-9e60-731066b8817e|Hello! my SNS3!!|2026-08-12 23:08:05|user1
e6d532c8-974b-4a01-89f6-6e1b2c4cff32|Hello Fediverse! #test|2026-08-22 02:16:04|user1
aea0ec89-a368-44a9-8493-b2bd82a797a8|post test! #test|2026-08-12 03:08:18|user1
sqlite> select * from inbox_posts;
sqlite> 

โœ”๏ธ ๊ธ€๋ฐ›๊ธฐ(inbox_posts ํ…Œ์ด๋ธ”์— ์ €์žฅ)
Receive posts (stored in the inbox_posts table)

— ๊ธ€ ์ž‘์„ฑ์€ pinafore(website)๋‚˜ Tusky(android),Feditex(IOS)์—์„œ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
You can create posts using Pinafore(website), Tusky (Android), or Feditex (iOS).

— ์„œ๋ฒ„ ๋กœ๊ทธ / Server Log

# server log

๐Ÿ“ฉ [user1] INBOX: Create https://freelifemakers.com/users/user1
๐Ÿ“ [user1] ์ƒˆ ๊ธ€ ๋„์ฐฉ / New post received from https://freelifemakers.com/users/user1
๋‚ด์šฉ/content: <p>inbox_posts table test</p>
โœ… [user1] inbox_posts ์ €์žฅ ์™„๋ฃŒ : https://freelifemakers.com/users/user1/statuses/01M0EJPP57WC47FAK0N395S7MB

— index_posts์— ๊ธ€ ์ €์žฅํ™•์ธ ํ•˜๊ธฐ
Check if posts are saved in index_posts

sqlite> select * from inbox_posts;
https://freelifemakers.com/users/user1/statuses/0--------7FAK0N395----|https://freelifemakers.com/users/user1|<p>inbox_posts table test</p>|user1|2026-08-20 03:16:15
sqlite> 
Terminal

์š”ํ•œ๋ณต์Œ 8์žฅ 32์ ˆ / John 8:32

“๊ทธ๋ฆฌ๊ณ  ๋„ˆํฌ๋Š” ์ง„๋ฆฌ๋ฅผ ์•Œ๊ฒŒ ๋  ๊ฒƒ์ด๋ฉฐ, ์ง„๋ฆฌ๊ฐ€ ๋„ˆํฌ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ํ•  ๊ฒƒ์ด๋‹ค.”

“Then you will know the truth ,and the truth will set you free”

Leave a Reply