๐๐ป myapp25์์๋ ๋ถ์คํธํ๋ ๊ฒฝ์ฐ ๋ด ํ๋ก์์๊ฒ ๊ธ์ ์ ๋ฌํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
In myapp25, we are implementing a feature that delivers posts to your followers when you “boost” them.
๐ ์ ์ฒด ํ๋ก์ ํธ ๊ตฌ์กฐ / Overall Project Structure
myapp24/
โโโ 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
โ โโโ 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
โ โโโ 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
๐ ํ๋ก์ ํธ ์์(myapp25)
Project Start (myapp25)
npx create-next-app@latest
๐ SQLite์ค์น / Installing SQLite
cd ~/myapp25
npm install better-sqlite3
npm install -D @types/better-sqlite3
๐ DDNS,https์ค์ / DDNS,https settings
๐ ๋๋ฉ์ธํ์ฉ, @userid ์ฌ์ฉํ๊ธฐ
Allowing domains, using @userid
— next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* allow domain*/
allowedDevOrigins: ['aloy-horizon.duckdns.org', '*.duckdns.org'],
/* host.domain.org/@userid */
async rewrites() {
return [
{
source: '/@:username',
destination: '/usersui/:username',
},
]
}
};
export default nextConfig;
๐ ๋ถ์คํธ ๋ณด๋ผ ๋ / When sending a boost
โ๏ธ myapp25/app/api/announce/route.ts
if (result.ok) {
db.prepare('INSERT INTO announces (id, username, object) VALUES (?, ?, ?)').run(result.announceDoc.id, username, apObjectId);
// โ
myapp25 - ํ๋ก์์๊ฒ ๋ถ์คํธ ๋ฐฐ๋ฌ / Deliver Boosts to followers
const followers = db.prepare('SELECT inbox, actor FROM followers WHERE username = ?').all(username) as any[];
console.log(`๐ข ๋ถ์คํธ ๋ฐฐ๋ฌ ์์: ${followers.length}๋ช
์๊ฒ`);
for (const f of followers) {
try {
// ํ๋ก์ inbox์ Announce ๊ทธ๋๋ก ์ ๋ฌ
// ap.ts์ sendAnnounce ์ฌ์ฌ์ฉ
await sendAnnounce(f.inbox, apObjectId, username);
} catch (e) {
console.error(`๋ฐฐ๋ฌ ์คํจ -> ${f.actor}`, e);
}
}
}
๐ ๋ถ์คํธ ๋ฐ์ ๋ / When receiving a boost
โ๏ธ myapp25/app/users/[username]/inbox/route.ts
— ๋ถ์คํธ ๋ฐ๊ธฐ / Receive boost
// โ
myapp25 - ๋ถ์คํธ ๋ฐ๊ธฐ / Receive Boost !
if (body.type === 'Announce') {
try {
const announceId = body.id;
const objectId = typeof body.object === 'string' ? body.object : body.object?.id;
console.log(`๐ [${username}] Announce ๋์ฐฉ: ${actorId} -> ${objectId}`);
// 1. announces ํ
์ด๋ธ์ ๊ธฐ๋ก
// Record in announces table
db.prepare('INSERT OR IGNORE INTO announces (id, username, object) VALUES (?,?,?)')
.run(announceId, username, objectId);
// 2. ์๊ธ ๋ด์ฉ ๊ฐ์ ธ์์ inbox_posts์ ์ ์ฅ (ํ์๋ผ์ธ์ ๋จ๊ฒ)
// Retrieve the original post content and save it to inbox_posts (so it appears on the timeline)
try {
const noteRes = await fetch(objectId, {
headers: { Accept: 'application/activity+json' }
});
if (noteRes.ok) {
const note = await noteRes.json();
const longId = note.id || objectId;
const shortId = `${longId.split('/').pop()}_boost_${Date.now()}`; // ๋ถ์คํธ๋ ๋ณ๋ id
const content = note.content || note.summary || '';
db.prepare(`
INSERT OR IGNORE INTO inbox_posts (id, actor, content, username, original_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(shortId, actorId, content, username, longId, body.published || new Date().toISOString());
console.log(`โ
[${username}] ๋ถ์คํธ inbox_posts ์ ์ฅ / Boost inbox_posts save: ${shortId}`);
}
} catch (fetchErr) {
console.log(`โ ์๊ธ fetch ์คํจ, content ์์ด ์ ์ฅ`, fetchErr);
// fetch ์คํจํด๋ ๋ถ์คํธ ๊ธฐ๋ก์ ๋จ๊น / Boost records are saved even if fetch fails.
db.prepare(`
INSERT OR IGNORE INTO inbox_posts (id, actor, content, username, original_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(`boost_${Date.now()}`, actorId, `[Boost] ${objectId}`, username, objectId, new Date().toISOString());
}
console.log(`โ
[${username}] announces ์ ์ฅ ์๋ฃ / Announcements saved.: ${announceId}`);
} catch (e) {
console.error(`โ Announce ์ ์ฅ ์คํจ / Failed to save Announce.`, e);
}
return new Response('', { status: 202 });
}
— ๋ถ์คํธ ์ทจ์๋ฐ๊ธฐ / Cancel Boost(Undo Announce)
// โ
myapp25 - Undo Announce (๋ถ์คํธ ์ทจ์ ๋ฐ๊ธฐ)
if (body.type === 'Undo') {
// ์์์ ์ด๋ฏธ Follow, Like Undo ์ฒ๋ฆฌํ์ผ๋๊น ์ฌ๊ธฐ์๋ Announce Undo๋ง
const obj = body.object;
if (typeof obj === 'object' && obj.type === 'Announce') {
const announceId = obj.id;
const objectId = typeof obj.object === 'string' ? obj.object : obj.object?.id;
console.log(`โฉ [${username}] Undo Announce: ${actorId} -> ${objectId}`);
db.prepare('DELETE FROM announces WHERE id = ?').run(announceId);
// inbox_posts์์ ๋ถ์คํธ ๊ธ๋ ์ญ์ (์ ํ)
db.prepare('DELETE FROM inbox_posts WHERE original_id = ? AND actor = ?').run(objectId, actorId);
console.log(`๐ [${username}] ๋ถ์คํธ ์ทจ์ ์ฒ๋ฆฌ ์๋ฃ / Boost processing cancellation complete.`);
return new Response('', { status: 202 });
}
}
๐ ํ ์คํธ / Test
# ํ๋ก์ฐ (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://mastodon.social/users/mcnees"}'
# ์ธํ๋ก์ฐ (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://mastodon.social/users/mcnees"}'
# ๋ถ์คํธ ์คํ / Boost
curl -X POST https://aloy-horizon.duckdns.org/api/announce \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089019419350090"}'
# ๋ถ์คํธ ์ทจ์ / Cancle Boost
curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089019419350090"}'
โ๏ธ ๋ง์คํ ๋ ์๋ฒ์ ๊ธ ๋ถ์คํธํ๊ธฐ / Boost a post on a Mastodon server
https://mastodon.social/@mcnees/117089019419350090
— ํ๋ก์ฐ,๋ถ์คํธ์คํ / Execute Follow and Boost
# ๋ง์คํ ๋ ์๋ฒ์ ํ๋ก์ฐ ์์ฒญ / Follow request to a Mastodon server
myapp25 % curl -X POST https://aloy-horizon.duckdns.org/api/follow \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://mastodon.social/users/mcnees"}'
{"ok":true,"inbox":"https://mastodon.social/users/mcnees/inbox","target":"https://mastodon.social/users/mcnees","username":"user1","result":"","follow":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/follows/1787878537250","type":"Follow","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/mcnees"}}%
# ๋ถ์คํธ ์คํ / Run Boost
myapp25 % curl -X POST https://aloy-horizon.duckdns.org/api/announce \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089019419350090"}'
{"ok":true,"result":{"ok":true,"status":202,"text":"","announceDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/announces/15aad35a-32de-4de0-b832-9134498b94d5","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/mcnees/statuses/117089019419350090","published":"2026-08-28T00:56:17.391Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers","https://aloy-horizon.duckdns.org/users/user1"]}}}%
gimdaegyeong@gimdaegyeong-ui-MacBookAir myapp25 %

— ๋ด DB์ ๋ถ์คํธ ์ ๋ฌํ์ธ / Check Boost delivery status in my DB
sqlite> select * from announces;
https://freelifemakers.com/users/user1/statuses/01M12WRSDN77HCBMPES6NH8A4Z|user1|https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG|2026-08-28 00:36:59
https://aloy-horizon.duckdns.org/users/user1/announces/9d4b9bc4-fa12-4073-b034-794c0dde40f8|user1|https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7|2026-08-28 00:46:46
https://aloy-horizon.duckdns.org/users/user1/announces/15aad35a-32de-4de0-b832-9134498b94d5|user1|https://mastodon.social/users/mcnees/statuses/117089019419350090|2026-08-28 00:56:17
sqlite>
โ๏ธ freelifemakers.com์์ ๋ถ์คํธ ํ ๊ฒฝ์ฐ
If you boosted via freelifemakers.com

sqlite> select * from announces;
https://freelifemakers.com/users/user1/statuses/01M12WRSDN77HCBMPES6NH8A4Z|user1|https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG|2026-08-28 00:36:59
โ๏ธ aloy-horizon.duckdns.org๊ฐ freelifemakers.com์ ๋ถ์คํธํ ๊ฒฝ์ฐ
If aloy-horizon.duckdns.org has boosted freelifemakers.com
# ๋ถ์คํธ ์คํ / Boost
curl -X POST https://aloy-horizon.duckdns.org/api/announce \
-H "Content-Type: application/json" \
-d
'{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7"}'
# ๋ถ์คํธ ์ทจ์ / Cancle Boost
curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
-H "Content-Type: application/json" \
-d
'{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7"}'
myapp25 % curl -X POST https://aloy-horizon.duckdns.org/api/announce \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7"}'
{"ok":true,"result":{"ok":true,"status":202,"text":"{\"status\":\"Accepted\"}","announceDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/announces/9d4b9bc4-fa12-4073-b034-794c0dde40f8","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7","published":"2026-08-28T00:46:46.182Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers","https://aloy-horizon.duckdns.org/users/user1"]}}}%
sqlite> select * from announces;
https://freelifemakers.com/users/user1/statuses/01M12WRSDN77HCBMPES6NH8A4Z|user1|https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG|2026-08-28 00:36:59
https://aloy-horizon.duckdns.org/users/user1/announces/9d4b9bc4-fa12-4073-b034-794c0dde40f8|user1|https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7|2026-08-28 00:46:46
sqlite>
์ํ๋ณต์ 8์ฅ 32์ / John 8:32
“๊ทธ๋ฆฌ๊ณ ๋ํฌ๋ ์ง๋ฆฌ๋ฅผ ์๊ฒ ๋ ๊ฒ์ด๋ฉฐ, ์ง๋ฆฌ๊ฐ ๋ํฌ๋ฅผ ์์ ๋กญ๊ฒ ํ ๊ฒ์ด๋ค.”
“Then you will know the truth ,and the truth will set you free”