ππ» νμκ°μ
λ° λ‘κ·ΈμΈκ³Ό κ΄λ ¨λ κΈ°λ₯μ
λλ€.
These are features related to sign-up and login.
ππ» pinaforeνλ©΄μμ νμ κ²λ€μ ꡬν μ€μ
λλ€.
I am currently implementing the elements displayed on the pinafore screen.
ππ» ννμλΌμΈμμ μ’μμμ λΆμ€νΈλ₯Ό νμν©λλ€.
Displays likes and boosts on the home timeline.
ππ» pinaforeμλ inbox_postsλ°μ΄ν° κ°―μκ° κ²μλ¬Ό κ°―μμ
λλ€.
In Pinafore, the number of posts corresponds to the count of inbox_posts data entries.
ππ» λΆμ€νΈλ κ²μλ¬Όμ μΆκ°λ‘ νμλΌμΈμ νμλμ§λ§ pinaforeμμλ νμλμ§ μμ΅λλ€.
Boosted posts additionally appear on the timeline, but they do not appear in Pinafore.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
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/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
β βββ 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
β β βββ _components/themes/
β β βββ themeex/ThemeexTheme.tsx -> Example Theme
β β βββ pinafore/PinaforeTheme.tsx -> Theme 1
β β βββ 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
β βββ 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
βοΈ pinafore ν νμλΌμΈμ λ΄κ° νλ‘μ° νλ μ¬λ λͺ¨λ κΈ λ¨κ² νκΈ°
Make posts from everyone I follow appear on my Pinafore home timeline.
— app/api/v1/timelines/home/route.ts
import { NextResponse } from 'next/server';
import db from '@/lib/db';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
function parseDate(v: any): number {
if (!v) return 0;
if (/^\d{13}$/.test(String(v))) return parseInt(v);
const d = new Date(v).getTime();
return isNaN(d)? 0 : d;
}
export async function GET(req: Request) { ...
// β
myapp36-μ 체 κ²μ / Full search
const reblogs_count = announcesByObject.get(longId) || announcesByObject.get(originalId) || announcesByObject.get(shortId) || announcesByObject.get(short8) || 0;
const favourites_count = likesByObject.get(longId) || likesByObject.get(originalId) || likesByObject.get(shortId) || likesByObject.get(short8) || 0;
const favourited = likedSet.has(longId) || likedSet.has(originalId) || likedSet.has(shortId) || likedSet.has(short8);
const reblogged = reblogs_count > 0;
let content = row.content || '';
try { if (content.startsWith('{')) { const o = JSON.parse(content); content = o.object?.content || o.content || content; } } catch {}
const baseAccount = isMy
? { id: '1', username, acct: `${username}@${DOMAIN}`, display_name: username, avatar: `https://${DOMAIN}/icon.png` }
: (() => {
let uname = 'user1', dom = 'freelifemakers.com', display = 'user1';
if (row.actor?.includes('mastodon.social')) { uname = 'freelifemakers'; dom = 'mastodon.social'; display = 'freelifemakers'; }
return { id: `remote_${dom.replace(/\./g,'_')}_${uname}`, username: uname, acct: `${uname}@${dom}`, display_name: display, avatar: `https://${DOMAIN}/icon.png` };
})();
return {
id: String(row.id),
uri: longId,
url: longId,
account: baseAccount,
content: content.startsWith('<')? content : `<p>${content}</p>`,
created_at: new Date(parseDate(row.created_at)).toISOString(),
visibility: 'public',
reblogs_count, // μ 체 λΆμ€νΈ μΉ΄μ΄νΈ / Full boost count
favourites_count, // μ 체 μ’μμ μΉ΄μ΄νΈ / Full like count
replies_count: 0,
favourited: !!favourited, // μ 체 μ’μμ / Full like
reblogged: !!reblogged, // μ 체 λΆμ€νΈ / Full Boost
muted: false, bookmarked: false, pinned: false,
};
... ...
}
βοΈ announce λ‘μ§ μμ – DBμμ announce(boost) μ μ₯νκΈ° μκΈ°
Modify ‘announce’ logic β do not save ‘announce’ (boost) to the database.
— announceμ μ μ₯λ κΈκ³Ό μΌμΉνλ inbox_posts κΈμ λΆμ€νΈ νμ
Display a boost indicator on inbox_posts entries that match posts stored in announce.
–app/users/[username]/inbox/route.ts
// Announce μμ - actor ν¬ν¨! / Receive Announce - include actor!
if (body.type === 'Announce') {
try {
const announceId = body.id;
let objectId = typeof body.object === 'string' ? body.object : body.object?.id;
if (!objectId) return new Response('', { status: 202 });
objectId = objectId.replace('/posts/', '/statuses/');
console.log(`π [${username}] Announce λμ°©: ${actorId} -> ${objectId}`);
// actor μ»¬λΌ ν¬ν¨
db.prepare('INSERT OR IGNORE INTO announces (id, actor, object, username) VALUES (?,?,?,?)')
.run(announceId, actorId, objectId, username);
// β
myapp36
// - announce(boost)λ inbox_postsμ μ μ₯ μ·¨μ,announceν
μ΄λΈ κ°κ³Ό κ°μκ² κ²μνκΈ°
// For `announce(boost)`, remove the entry from `inbox_posts` and search for records matching the values ββin the `announce` table.
// 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()}`;
// const content = note.content || '';
// 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());
// }
// } catch {}
console.log(`β
announces μ μ₯ μλ£ / announces saved: ${announceId}`);
} catch (e) {
console.error(`β Announce μ μ₯ μ€ν¨ / Failed to save announce`, e);
}
return new Response('', { status: 202 });
}
π ν μ€νΈ / Test
βοΈ pinafore.social
— inbox_posts + postsμ κΈμ νμΈν©λλ€.

βοΈ aloy-horizon.duckdns.org/@user1
— λͺ¨λ λ°μ΄ν°μ μΉ΄μ΄νΈλ μ¬κΈ°μ νμΈ ν μ μμ΅λλ€.
You can check all the data and counts here.

μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”