๐๐ป ์ด๋ฒ ํฌ์คํธ๋ ํ๋ก์ ๋ฆฌ์คํธ๋ฅผ ๊ตฌํํ๋ ๋ถ๋ถ์
๋๋ค.
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

โ๏ธ ํฐ๋ฏธ๋์์ ํ๋ก์ฐ,์ธํ๋ก์ฐ ,ํ๋ก์ ๋ฆฌ์คํธ๋ณด๊ธฐ
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>

์ํ๋ณต์ 8์ฅ 32์ / John 8:32
“๊ทธ๋ฆฌ๊ณ ๋ํฌ๋ ์ง๋ฆฌ๋ฅผ ์๊ฒ ๋ ๊ฒ์ด๋ฉฐ, ์ง๋ฆฌ๊ฐ ๋ํฌ๋ฅผ ์์ ๋กญ๊ฒ ํ ๊ฒ์ด๋ค.”
“Then you will know the truth ,and the truth will set you free”