[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