๐๐ป myapp45์์๋ SNS ํ์ด์ง์ ๊ธ ์ญ์ ๋ฒํผ์ ์ถ๊ฐํฉ๋๋ค.
In myapp45, a delete button for posts is added.
๐๐ป ๊ธ์ญ์ ์ ์ฌ๋ฐ๋ฅธ ํค๋ก ์ญ์ ๊ฐ๋ฅํ๋๋ก ์์ ํ์ต๋๋ค.
Modified the post deletion process to ensure deletion is possible using the correct key.
— ๋ก๊ทธ์ธ ๊ตฌํ์ ํค ๊ฒฝ๋ก ๋ณ๊ฒฝ๋ ๋ถ๋ถ
Changes to key paths during login implementation
๐๐ป ํ์ฌ ๋ก์ปฌ์ ๊ธ ์ ์ฅ ์ญ์ ๊ฐ ๋๋ ๊ธ๋ฐฐ๋ฌ ๊ธฐ๋ฅ๊ณผ pinafore์์ ๊ธ ์ญ์ ๊ธฐ๋ฅ์ ์์ง ๊ตฌํ์ค์
๋๋ค.
Currently, saving and deleting posts works in the local environment, but the post delivery function and the post deletion feature in Pinafore are still under development.
— activitypub : app/api/v1/statuses/route.ts
— local : users/[username]/route.ts
๐๐ป ์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธ ํ ์ ์์ต๋๋ค.
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/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/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
โ โโโ 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
๐ ๋ ๊ฑฐ์ ํค๋ฅผ user1์ผ๋ก ๋ณ๊ฒฝ
Change the legacy key to user1.
— ์ฐ๋ฐฉ์๋ฒ์ ์์ฑ๋ ๊ธ์ ๋ ๊ฑฐ์ํค๋ก ์์ฑ์ด ๋์ด ์์ต๋๋ค.
Posts created on the federation server are written using legacy keys.
— ํ์ฌ user1์ ํค๋ ์๋ก ๋ฐ๊ธ๋ ์ํ์
๋๋ค.
The key for user1 has currently been newly issued.
— ๊ทธ๋์ ๋ํค๊ฐ ๋ง์ง์์ ์๋ก ๋ฐ๊ธ๋ ํค๋ฅผ ๋ฐฑ์
ํ๊ณ ๋ ๊ฑฐ์ํค๋ก ๋์ฒด ํฉ๋๋ค.
Since the key did not match, I backed up the newly issued key and replaced it with the legacy key.
# ์ ๊ทํค ๋ฐฑ์
๋ฐ ๋ ๊ฑฐ์ํค๋ฅผ user1 ํค ๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌ
# Back up the new key and copy the legacy key to the user1 key directory.
# ๋ฐฑ์
/ backup
myapp45 % cp data/keys/user1/private.pem data/keys/user1/private.pem.bak
myapp45 % cp data/keys/user1/public.pem data/keys/user1/public.pem.bak
# ๋ ๊ฑฐ์ํค๋ฅผ user1 ํค ๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌ
# Copy the legacy key to the user1 key directory.
myapp45 % cp data/keys/private.pem data/keys/user1/private.pem
% cp data/keys/public.pem data/keys/user1/public.pem
# ํ์ธ / check
myapp45 % diff data/keys/private.pem data/keys/user1/private.pem && echo "โ
same key" || echo "โ diffrent key"
โ
same key
myapp45 %
# DB๋ ์์ / # DB also updated
sqlite3 db.sqlite "UPDATE users SET public_key = '$(cat data/keys/user1/public.pem)' WHERE username='user1';"
๐ ์ฝ๋ ์์ / Code modification
๐ก ์๋์ ํ
๋งํ์ผ ์์ ์ด์ ๊น์ง ๋จ๋ฝ์ ๋ด๊ฐ ์์ฑํ ๊ธ์ ์ญ์ ๋ฒํผ์ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
The section below, prior to the theme file modification, implements a feature that displays a delete button on posts I have written.
โ๏ธ API – app/api/posts/route.ts
— ๋ผ์ฐํธ ์์ , ๋ชจ๋ ํจ์๋ฅผ ํ ํฐ์ ์ ์ฉํ๋๋ก ์์
Modified routes and updated all functions to implement token-based authentication.
— inbox_posts ํ
์ด๋ธ๋ ๋ด์ฉ๋ ์ญ์ ํ๋๋ก ์์
Modified to delete both the inbox_posts table and its contents.
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
// โ
myapp45 - ํ ํฐ ์ ์ฉ / token apply
let username = searchParams.get('username');
const auth = req.headers.get('Authorization') || '';
const token = auth.replace('Bearer ', '').trim();
if (token) {
const oauth = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
if (oauth?.username) username = oauth.username; // If you have a token username > tokenuser
}
if (!username) return NextResponse.json({ error: 'username required' }, { status: 401 });
... ...
}
export async function POST(req: Request) {
let { content, username } = await req.json();
// โ
myapp45 - apply token
const auth = req.headers.get('Authorization') || '';
const token = auth.replace('Bearer ', '').trim();
if (token) {
const oauth = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
if (oauth?.username) username = oauth.username; // ํ ํฐ ์์ผ๋ฉด ํ ํฐ ์ ์ ๋ก ๋ฎ์ด์ฐ๊ธฐ!
}
if (!username) return NextResponse.json({ error: '๋ก๊ทธ์ธ ํ์ / username required' }, { status: 400 });
if (!content) return NextResponse.json({ error: '๋ด์ฉ ์์ / Content required' }, { status: 400 });
...... }
export async function PUT(req: Request) {
// โ
myapp45 - apply token
let { id, content, username } = await req.json();
const auth = req.headers.get('Authorization') || '';
const token = auth.replace('Bearer ', '').trim();
if (token) {
const oauth = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
if (oauth?.username) username = oauth.username; // ํ ํฐ ์์ผ๋ฉด ํ ํฐ ์ ์ ๋ก ๋ฎ์ด์ฐ๊ธฐ!
}
... ...
}
export async function DELETE(req: Request) {
const { searchParams } = new URL(req.url);
let id = searchParams.get('id'); // defalut method is GET
let username = searchParams.get('username');
// โ
myapp45 - apply token
const auth = req.headers.get('Authorization') || '';
const token = auth.replace('Bearer ', '').trim();
if (token) {
const oauth = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
if (oauth?.username) username = oauth.username; // ํ ํฐ ์์ผ๋ฉด ํ ํฐ ์ ์ ๋ก ๋ฎ์ด์ฐ๊ธฐ!
}
... ...
// โ
myapp45 - inbox_posts ์ญ์ / Delete inbox_posts
db.prepare('DELETE FROM inbox_posts WHERE original_id LIKE ? OR id LIKE ? OR original_id = ?').run(`%${id}%`, `%${id}%`, id);
... ...
}
โ๏ธ API – /app/api/timeline/route.ts
— app/usersui/[username]/ClientPage.tsx์์ ํธ์ถํ๋ API
API called by app/usersui/[username]/ClientPage.tsx
— isMine , isBoostedPost
— isBoostedPost๋ ์ฌํ์ธ ์์
isBoostedPost is subject to re-verification.
... ...
const enriched = timeline.map((p: any) => {
// โ
myapp45 - ๋ค๋ฅธ ์ฌ๋์ด ๋ถ์คํธํ ๊ธ ์ฒดํฌ ์ถ๊ฐ / Added a check for posts boosted by others.
// - inbox_posts ํ
์ด๋ธ ๊ฒ์์ธ ๊ฒฝ์ฐ / In the case of a search on the `inbox_posts` table
const isBoostedPost = p.source === 'inbox' && !!p.original_id && p.original_id !== p.id;
// - posts ํ
์ด๋ธ ๊ฒ์์ธ ๊ฒฝ์ฐ / In the case of a search on the `posts` table
const isMine = p.source === 'mine';
... ...
return {
...p,
fullId,
isMyBoost,
boostCount,
isMyLike,
likeCount,
isBoostedPost, // inbox_posts ์ค ๋ถ์คํธ์ธ์ง ํ์ธ / Check if the inbox_posts item is a boost.
isMine, // ๋ด๊ฐ ์ด ๊ธ๋ง ์ญ์ ๊ฐ๋ฅ / You can only delete posts you have written.
};
});
โ๏ธ API – users/[username]/route.ts
— lib/ap.ts ํจ์๋ฅผ ์ฌ์ฉํ๋๋ก ์์
Modified to use the function in lib/ap.ts
... ...
import { getPublicKeyPemForServing } from '@/lib/ap';
export async function GET(_req: Request, { params }: { params: Promise<{ username: string }> }) {
const { username } = await params;
const domain = 'aloy-horizon.duckdns.org';
//const pubkey = fs.readFileSync(path.join(process.cwd(), 'data/keys/public.pem'), 'utf8');
const pubkey = getPublicKeyPemForServing(username);
... ...
}
โ๏ธ UI – /app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx
— ํ ๋งํ์ผ ์ ๋๋ค. / This is a theme file.
... ...
// โ
myapp45 - timeline == iniitialTimeline(like alias, not type)
export function PinaforeTheme({ timeline : initialTimeline, username, initialView, onBoost, onLike }: any) {
... ....
// โ
myapp45 - initialTimeline ๊ฐ์ด ๋ฐ๋๋ฉด UI ๋ค์ ๊ทธ๋ฆฌ๊ธฐ
// Redraw the UI when the initialTimeline value changes.
const [timeline, setTimeline] = useState<any[]>(initialTimeline || []);
useEffect(() => { setTimeline(initialTimeline || []); }, [initialTimeline]);
... ...
// โ
myapp45: ์ญ์ ํธ๋ค๋ฌ / Delete handler
const handleDelete = async (postId: string) => {
if (!confirm('์ญ์ ํ ๊น? / Delete?')) return;
try {
// ๊ธฐ์กด DELETE ๋ผ์ฐํธ ์ฌ์ฉ / Use the existing DELETE route.
const res = await fetch(`/api/posts?id=${postId}&username=${currentUser}`, {
method: 'DELETE',
credentials: 'include'
});
if (res.ok) {
setTimeline((prev: any[]) => prev.filter((t: any) => t.id !== postId));
console.log(`๐๏ธ Delete ${postId} ok`);
} else {
alert('์ญ์ ์คํจ / Deletion failed');
}
} catch {
alert('์ญ์ ์คํจ / Deletion failed');
}
};
... ...
<div className="status-header">
{/* - Actor */}
<b>{getDisplayName(p.actor)}</b>
{/* - boosted- */}
<span className="status-time">{new Date(p.created_at).toLocaleString()}</span>
{p.source === 'inbox' && <span className="boost-label">๐ boosted</span>}
{/* - โ
myapp45 - Delete Posts - */}
{currentUser && p.isMine && (
<button onClick={() => handleDelete(p.id)} style={{ color: '#e0245e', marginLeft: 'auto' }}>
๐ Delete
</button>
)}
</div>
}
๐ก ์ฌ๊ธฐ์ ๋ถํฐ๋ ์ค๋ฅ ์์ ์ ์ํด์ ํ์ผ ์์ ๋ ๋ถ๋ถ์
๋๋ค.
The following section contains modifications made to the file to fix errors.
โ๏ธ lib/ap.ts
— ์ธ๋ถ์์ ํจ์ ์ฌ์ฉ๊ฐ๋ฅํ๋๋ก exportํจ์ ์ถ๊ฐ
Added an export function to make the function accessible from the outside.
// โ
myapp31 - ์ฌ์ฉ์๋ณ ๊ณต๊ฐํค ๊ฐ์ ธ์ค๊ธฐ / Get user-specific public key
function getPublicKey(username: string): string {
const userKeyPath = path.join(process.cwd(), `data/keys/${username}/public.pem`);
const legacyKeyPath = path.join(process.cwd(), 'data/keys/public.pem');
const directPath = path.join(process.cwd(), `data/keys/${username}.pub.pem`);
try {
if (fs.existsSync(userKeyPath)) {
return fs.readFileSync(userKeyPath, 'utf8');
}
if (fs.existsSync(legacyKeyPath)) {
return fs.readFileSync(legacyKeyPath, 'utf8');
}
if (fs.existsSync(directPath)) {
return fs.readFileSync(directPath, 'utf8');
}
// DB์์ fallback! / Fallback from DB!
const user = db.prepare('SELECT public_key FROM users WHERE username = ?').get(username) as any;
if (user?.public_key) return user.public_key;
} catch (e) {
console.error(`๐ public ํค ๋ก๋ ์คํจ / Public key load failed for ${username}:`, e);
}
throw new Error(`Public key not found for ${username}`);
}
// โ
myapp45 - for mastodon
export function getPublicKeyPemForServing(username: string): string {
return getPublicKey(username); // ํ์ผ ์ฐ์ ! DB fallback
}
// โ
๊ณต์ฉ - ์๋ช
+ ์ ์ก / Shared Use - Sign and send
async function signAndSend(inbox: string, doc: any, username: string) {
const actorId = `https://${DOMAIN}/users/${username}`;
const privateKey = getPrivateKey(username);
const body = JSON.stringify(doc);
const url = new URL(inbox);
const digest = `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
const date = new Date().toUTCString();
// โ
myapp45 - search ํฌํจ + ์๋ฌธ์ post!
const target = `${url.pathname}${url.search}`;
const signingString = `(request-target): post ${target}\nhost: ${url.host}\ndate: ${date}\ndigest: ${digest}`;
const signer = crypto.createSign('sha256');
signer.update(signingString);
const signature = signer.sign(privateKey, 'base64');
const keyId = `${actorId}#main-key`;
// โ
myapp45 - algorithm ์ถ๊ฐ ๋ง์คํ ๋ ํ์
const sigHeader = `keyId="${keyId}",headers="(request-target) host date digest",signature="${signature}",algorithm="rsa-sha256"`;
console.log(`๐ SIGN ${username} -> ${inbox}\n${signingString}\n`); // ๋๋ฒ๊ทธ์ฉ!
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();
return { res, text, body, doc };
}
โ๏ธ users/[username]/route.ts
— lib/ap.tsํ์ผ์ ์ฌ์ฉํ๋๋ก ์์ ํจ.
Modified to use the lib/ap.ts file.
... ...
import { getPublicKeyPemForServing } from '@/lib/ap';
... ...
export async function GET(_req: Request, { params }: { params: Promise<{ username: string }> }) {
... ...
//const pubkey = fs.readFileSync(path.join(process.cwd(), 'data/keys/public.pem'), 'utf8');
const pubkey = getPublicKeyPemForServing(username);
... ...
}
๐ ํ ์คํธ / Test
๐ก ํ์ฌ ๋ก์ปฌ์๋ฒ์์ ๊ธ ์ ์ฅ ์ญ์ ๊ธฐ๋ฅ์ ์ ์์ ์ผ๋ก ์๋ํฉ๋๋ค.
The functions for saving and deleting posts are currently working correctly on the local server.
๐ก ์์ง ํน์ ์๋ฒ์ ๊ธ๋ฐฐ๋ฌ์ด ๋์ง ์๋ ๋ฌธ์ ๋ด ๊ธ ์ญ์ ์ ๋ฐฐ๋ฌ๋ ์ฐ๋ฐฉ์๋ฒ์ ๊ธ์ด ์ญ์ ๋์ง ์๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
There are currently issues where posts are not being delivered to certain servers, and where posts delivered to federated servers are not deleted when the original post is removed.
โ๏ธ ๋ก๊ทธ์ธ ํ ์ญ์ ๋ฒํผ ์ ์ฉ

โ๏ธ ๊ธ ๋ฐฐ๋ฌํ์ธ / Message Delivery Confirmation
— ํ์ฌ๋ gotosocial๋ง ๋ฐฐ๋ฌ๋จ / Currently, only GoToSocial is being deployed.


โ๏ธ ๊ธ ์ญ์ / delte post
— ์ฐ๋ฐฉ๊ธ ์ญ์ ๋ ํ์ฌ ์์ฒญ๋ง ์ ๋ฌ๋จ.
The request to delete the syndicated post has been forwarded.

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