ππ» myapp49μμλ myapp48κΉμ§ κΈ°λ₯ μΆκ° λλ©΄μ λ°μν μ€λ₯λ λ²κ·Έλ₯Ό μμ ν©λλ€.
myapp49 fixes errors and bugs that arose during the addition of features up to myapp48.
ππ» λΆμ€νΈ ,μ’μμ λ°°λ¬ λΆλΆ μ€λ₯λ₯Ό μμ ν©λλ€.
Fixed errors related to Boosts, Likes, and delivery.
ππ» μΈλΆμ±μμ κΈμ°κ±°λ μμ νλ APIλ₯Ό ν°λ―Έλμμ μ¬μ©ν κ²½μ° ν ν°μ μ
λ ₯ν΄μΌν©λλ€.
You must enter a token when using the API to create or delete posts from an external app via the terminal.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
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
β βββ visibility.ts -> Visibility(Public,Unlisted,Private,Direct)
β βββ 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
π μ½λ μμ / Code Modification
βοΈ λ΄ μλ²μμ μλ³ΈκΈ μμ μ 리λͺ¨νΈ μλ²μμ μμ λμ§ μλ λ¬Έμ μμ
Fixed an issue where deleting the original post on my server did not delete it from the remote server.
— app/api/posts/route.ts
//const noteId = `https://${DOMAIN}/users/${username}/posts/${id}`; // β
μ 체 URL λ§λ€κΈ° / Create full URL
// β
myapp49 - bug fix
const noteId = `https://${DOMAIN}/users/${username}/statuses/${id}`;
βοΈ λΆμ€νΈ λ° μ’μμ μ€λ₯ μμ
Fixed Boost and Like errors.
— κ³ ν¬μμ
μλ³Έ κΈμ μ’μμ,λΆμ€νΈλ₯Ό λ΄ μλ²μ λ°°λ¬λ κΈμ μ€ννκ²½μ° μλ³Έ κΈ μμ μ λ΄μλ²μ λ°°λ¬λκΈκ³Ό μ’μμ,λΆμ€νΈλ κ°μ΄ μμ
If you “like” or “boost” an original GoToSocial post and that action is applied to the copy delivered to your server, deleting the original post will also delete the delivered copy along with the associated likes and boosts.
if (body.type === 'Delete') {
... ...
if (objectId) {
... ...
// β
myapp49 - λ°°λ¬λ κΈμ μλ³Έ μμ μ λΆμ€νΈ μ’μμ μμ
// Deleting the original delivered post removes the 'Boost Like'.
// - μ’μμ μ 리 / delete likes
const likeDel = db.prepare(`DELETE FROM likes WHERE object LIKE? OR object LIKE?`).run(`%${shortId}%`, `%${canonicalId}%`);
if (likeDel.changes > 0) {
console.log(` -> likes μ 리: ${likeDel.changes}κ° μμ (μλ³Έ ${shortId})`);
}
// - λΆμ€νΈ μ 리 / delete boost(announce)
const announceDel = db.prepare(`DELETE FROM announces WHERE object LIKE? OR object LIKE?`).run(`%${shortId}%`, `%${canonicalId}%`);
if (announceDel.changes > 0) {
console.log(` -> announces μ 리: ${announceDel.changes}κ° μμ (μλ³Έ ${shortId})`);
}
... ...
}
... ...
}
βοΈ λΆμ€νΈ λΌλ²¨ μμ / Edit Boost Label
— inbox_postsλ‘ λ°°λ¬λλ λͺ¨λ κΈμ boosted λΌλ²¨μ΄ λΆλ λ¬Έμ μμ
Fixed an issue where all posts delivered to inbox_posts were labeled as “boosted.”
const boostCount = (db.prepare(
`SELECT COUNT(*) as c FROM announces WHERE object =? OR object =? OR object =? OR object LIKE '%' ||? || '%'`
).get(fullId, p.original_id || '', p.id || '', shortId) as any)?.c || 0;
// β
myapp49- announces.objectμ μλ³Έ μμΌλ©΄ true (λ€κ° λ§ν λ‘μ§)
const isBoostedPost = boostCount > 0 ? true : false;
1)announcesν
μ΄λΈμμ idλ λΆμ€νΈνμ¬λ μ 체 URL,objectλ μλ³ΈκΈURL, actorλ λΆμ€νΈν μ¬μ©μ κ³μ μ£Όμ
In the announces table, id is the full URL of the boost, object is the URL of the original post, and actor is the account address of the user who performed the boost.
2) announcesν
μ΄λΈμ objectνλ, inbox_posts ν
μ΄λΈμ id, original_idλ μλ³Έ κΈμ κ°λ¦¬ν΄
The object field in the announces table, as well as the id and original_id fields in the inbox_posts table, refer to the original post.
βοΈ μ’μμ μ€ν λ° μ’μμμ·¨μ
Like and Unlike
— μ’μμ λΆλΆμμ λ΄ κΈμΈμ§ 체ν¬νλ λΆλΆ λλ©μΈμΆκ°
Added the domain check to verify if the post belongs to the user in the “Like” section.
— λ°°μ‘λ μ’μμ μμ λ‘μ§ μμ
Fixed logic for deleting delivered ‘likes’
— app/api/like/route.ts
export async function POST(req: NextRequest) {
... ...
const isOwnPost = target.includes(`${DOMAIN}/users/${username}/`); // β
myapp49 λλ©μΈμΆκ° / add DOMAIN
... ...
}
export async function DELETE(req: NextRequest) {
const { username, target } = await req.json();
const myActor = `https://${DOMAIN}/users/${username}`;
const shortId = target.replace('/posts/', '/statuses/').split('/').pop()!.split('?')[0];
const likeRow = db.prepare(`SELECT id, object FROM likes WHERE actor =? AND object LIKE '%' ||? || '%'`).get(myActor, shortId) as any;
if (!likeRow) {
const likeCount = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`).get(shortId) as any).c;
return NextResponse.json({ ok: true, alreadyUnliked: true, isMyLike: false, likeCount });
}
const objectForCheck = likeRow.object || target;
const isOwnPost = objectForCheck.includes(`${DOMAIN}/users/${username}/`);
// β
myapp49 - μ’μμ λ°°λ¬ / Send a Like
if (!isOwnPost) {
try {
const u = new URL(objectForCheck);
const parts = u.pathname.split('/').filter(Boolean);
const idx = parts.indexOf('users');
const inbox = idx!== -1? `${u.origin}/users/${parts[idx+1]}/inbox` : null;
if (inbox) {
await sendUndoLike(inbox, likeRow.id, objectForCheck, username);
}
} catch (e) {
console.error('Undo Like λ°°λ¬ μ€ν¨', e);
}
}
db.prepare('DELETE FROM likes WHERE id =?').run(likeRow.id);
const likeCount = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`).get(shortId) as any).c;
return NextResponse.json({ ok: true, isMyLike: false, likeCount });
}
βοΈ λΆμ€νΈ μ€ν λ° λΆμ€νΈ μ·¨μ
Start Boost and Cancel Boost
— μ’μμ λΆλΆμ λ΄ κΈ μΈμ§ 체ν¬νλ λΆλΆ λλ©μΈμΆκ°
Added a domain check to the “Like” section to verify if the post belongs to the user.
— λ°°μ‘λ λΆμ€νΈ μμ λ‘μ§ μμ
Modified logic for deleting delivered boosts
— app/api/announces/route.ts
export async function POST(req: NextRequest) {
... ...
const isOwnPost = target.includes(`${DOMAIN}//users/${username}/`); // β
myapp48 λλ©μΈμΆκ° / Add DOMAIN
... ...
}
export async function DELETE(req: NextRequest) {
try {
const { username, target } = await req.json();
const myActor = `https://${DOMAIN}/users/${username}`;
const shortId = target.replace('/posts/', '/statuses/').split('?')[0].split('#')[0].split('/').filter(Boolean).pop() || target;
const row = db.prepare(
`SELECT id, object, actor FROM announces WHERE username =? AND object LIKE '%' ||? || '%'`
).get(username, shortId) as any;
if (!row) {
const boostCount = (db.prepare(
`SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`
).get(shortId) as any).c;
return NextResponse.json({ ok: true, alreadyUnboosted: true, isMyBoost: false, boostCount });
}
// λΆμ€νΈ μ·¨μ λ°°λ¬ / Undo Boost Delivery
const objectForCheck = row.object || target;
const isOwnPost = objectForCheck.includes(`${DOMAIN}/users/${username}/`);
if (!isOwnPost) {
try {
const u = new URL(objectForCheck);
const parts = u.pathname.split('/').filter(Boolean);
const idx = parts.indexOf('users');
const inbox = idx!== -1? `${u.origin}/users/${parts[idx+1]}/inbox` : null;
if (inbox) {
await sendUndoAnnounce(inbox, row.id, objectForCheck, username);
}
} catch (e) {
console.error('Undo Announce λ°°λ¬ μ€ν¨', e);
}
}
db.prepare('DELETE FROM announces WHERE id =?').run(row.id);
const boostCount = (db.prepare(
`SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`
).get(shortId) as any).c;
return NextResponse.json({ ok: true, isMyBoost: false, boostCount });
} catch (e: any) {
console.error('[Boost DELETE]', e);
return NextResponse.json({ ok: false, error: e.message }, { status: 500 });
}
}
βοΈ λΌμ°νΈ 보μκ°ν / Route Security Enhancement
— ν°λ―Έλμμ ν ν°μ΄λ μΏ ν€ μμ΄ κΈμ°κ±°λ μμ λ₯Ό ν μ μμ΅λλ€.
You cannot write or delete posts from the terminal without a token or cookie.
— app/api/v1/statuses/route.ts — Pinafore
export async function POST(req: Request) {
... ...
// β
myapp49 - pinafore,local server login
let username: string | null = null;
// β
myapp49 - 1. Bearer ν ν° μ²΄ν¬ / Bearer token check (Pinafore)
if (token) {
try {
const oauthToken = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
if (oauthToken?.username) username = oauthToken.username;
} catch {}
}
// 2. μΏ ν€ μ²΄ν¬ (λ‘컬 μλ²) / Cookie Check (Local Server)
if (!username) {
try {
const { getViewerFromRequest } = await import('@/lib/visibility');
username = getViewerFromRequest(req);
} catch {}
}
// β
3. λ λ€ μμΌλ©΄ 401 - 'user1' ν΄λ°± μ λ κΈμ§
// If neither exists, return 401βfalling back to 'user1' is strictly prohibited.
if (!username) {
return NextResponse.json({ error: 'unauthorized - Bearer token or login required' }, { status: 401 });
}
... ...
}
— app/api/v1/statuses/[id]/route.ts – Pinafore
export async function DELETE(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
... ...
// β
myapp49 - 1. μΈμ¦ - Bearer + μΏ ν€ λ λ€ μ§μ / Authentication - Supports both Bearer and cookies.
const auth = req.headers.get('Authorization') || '';
const token = auth.replace('Bearer ', '');
let username: string | null = null;
if (token) {
try {
const oauthToken = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
if (oauthToken?.username) username = oauthToken.username;
} catch {}
}
if (!username) {
try {
const { getViewerFromRequest } = await import('@/lib/visibility');
username = getViewerFromRequest(req as any);
} catch {}
}
// let oauthToken: any;
// try { oauthToken = db.prepare('SELECT * FROM oauth_tokens WHERE access_token=?').get(token) as any; } catch {}
// const username = oauthToken?.username || 'user1';
// β
myapp49 - ν΄λ°± κΈμ§ - μΈμ¦ μμΌλ©΄ 401 / Disable fallback β 401 if unauthenticated
if (!username) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401, headers: { 'Access-Control-Allow-Origin': '*' } });
}
console.log(`π Pinafore μμ μμ² / Request to delete Pinafore : ${username} -> ${id}`);
// β
myapp49 - μμ κΆ μ²΄ν¬ - λ΄ κΈ λ§λμ§ νμΈ / Ownership Check β Verify if it is my post
let post: any;
try {
post = db.prepare('SELECT id, username, visibility FROM posts WHERE id=?').get(id) as any;
} catch {}
if (!post) {
// μ΄λ―Έ μ§μμ‘μΌλ©΄ Pinaforeλ 404λ³΄λ€ 200μ μν¨ - λ©±λ±μ±
return NextResponse.json({ id, deleted: true, alreadyDeleted: true }, {
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' }
});
}
if (post.username !== username) {
console.log(`π« μμ κ±°λΆ: ${username}κ° ${post.username} κΈ μμ μλ`);
return NextResponse.json({ error: 'forbidden - not your post' }, {
status: 403,
headers: { 'Access-Control-Allow-Origin': '*' }
});
}
... ...
}
π ν μ€νΈ / Test
βοΈ μ’μμ / Likes


βοΈ λΆμ€νΈ / Boost


βοΈ api/v1/statuses λΌμ°νΈ ν°λ―Έλ ν
μ€νΈ
Terminal test for the api/v1/statuses route
# 1.κΈμ°κΈ° ν
μ€νΈ - ν ν°μ΄λ μΏ ν€ μλ κ²½μ°
Writing Test - No tokens or cookies
curl -s -X POST https://aloy-horizon.duckdns.org/api/v1/statuses \
-H "Content-Type: application/json" \
-d '{"status":"write post test-1","visibility":"public"}'
# μλ΅ / Response
{"error":"unauthorized - Bearer token or login required"}%
# 2.κΈμ°κΈ° ν
μ€νΈ / Test - refresh token
curl -s -X POST https://aloy-horizon.duckdns.org/api/v1/statuses \
-H "Cookie: refresh_token=099b61b8616edcf48b ... ..." \
-H "Content-Type: application/json" \
-d '{"status":"refresh token","visibility":"public"}'
# μλ΅ / Response
{"id":"c3b1cefe-853c-476e-b978-3b2e28506a92","uri":"https://aloy-horizon.duckdns.org/users/user1/statuses/c3b1cefe-853c-476e-b978-3b2e28506a92","url":"https://aloy-horizon.duckdns.org/users/user1/statuses/c3b1cefe-853c-476e-b978-3b2e28506a92","account":{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"},"content":"<p>refresh token</p>","created_at":"2026-09-26T04:26:48.469Z","visibility":"public","reblogs_count":0,"favourites_count":0,"replies_count":0}%
# 3. κΈ μμ ν
μ€νΈ - ν ν°μ΄λ μΏ ν€κ° μλκ²½μ°
myapp49 % curl -s -X DELETE https://aloy-horizon.duckdns.org/api/v1/statuses/9a9f4c12-265c-46ed-b956-f51a5e61fdb2
# μλ΅ / Response
{"error":"unauthorized"}%
# 4. κΈ μμ ν
μ€νΈ - refresh token
curl -s -X DELETE https://aloy-horizon.duckdns.org/api/v1/statuses/9a9f4c12-265c-46ed-b956-f51a5e61fdb2 \
-H "Authorization: Bearer 099b61b8616edcf48b4cf7260ef818b7a451... ..."
# μλ΅ / Response
{"id":"9a9f4c12-265c-46ed-b956-f51a5e61fdb2","deleted":true}%
π‘ ν°λ―Έλμμ μ§μ κΈμ°κΈ° / Writing directly in the terminal
curl -X POST https://aloy-horizon.duckdns.org/users/user1/outbox \
-H "Content-Type: application/activity+json" \
-d '{"type":"Create","object":{"content":"Hello Activity UI #activityposts "}}'
π‘ ν°λ―Έλμμ μ§μ κΈ μμ / Delete posts directly from the terminal.
curl -X DELETE "https://aloy-horizon.duckdns.org/api/cleanup-orphan?id=c429765e-1ffd-4168-b487-e4b36a7a162a&username=user1"
π‘ μ’μμ λ° μ’μμ μ·¨μμ€ν / Perform Like and Unlike actions
# μ’μμ μ€ν / Likes
curl -X POST https://aloy-horizon.duckdns.org/api/like \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}'
# μ’μμ μ·¨μνκΈ° / Undo Likes
curl -X DELETE https://aloy-horizon.duckdns.org/api/like \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}'
μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”