ππ» myapp57 μ κΈ°μ‘΄ snsν¬μ€νΈμ λκΈ κΈ°λ₯μ μΆκ°ν©λλ€.
myapp57 adds a commenting feature to existing social media posts.
ππ» νΈλ¦¬ ννλ‘ κΈ νλ¨μ λ¬Άμ΄λ κ΅¬μ‘°κ° μλλΌ μκΈ μ²λΌ λ¨κ³ μλ³ΈκΈμ μΆκ°λλ κΈμ νμνκΈ° μν΄μ @userid postνμμ νμλλ κΈ°λ₯μ
λλ€.
Instead of being grouped in a tree structure at the bottom of the post, this feature displays the content in the @userid post format, making it appear like a new post while indicating that it is an addition to the original post.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
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(deprecated)
β βββ 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
β βββ api/v1/media/route.ts -> Image upload
β βββ api/v1/media/[id]/route.ts -> Image Delete
β βββ api/v1/instance/rules/route.ts -> Feature not implemented.
β βββ api/v1/instance/domain_blocks/route.ts -> Feature not implemented.
β βββ api/v1/filters/route.ts -> Feature not implemented.
β βββ api/v1/lists/route.ts -> Feature not implemented.
β βββ api/v1/announcements/route.ts -> Feature not implemented.
β βββ api/v1/timelines/public/route.ts -> Feature not implemented.
β βββ api/v1/statuses/[id]/context/route.ts -> Post Check,Delete
β βββ oauth/authorize/route.ts -> auth
β βββ oauth/token/route.ts -> auth
β βββ oauth/login-and-authorize/route.ts -> Tursky,Pinafore login processing
β βββ oauth/revoke/route.ts -> Feature not implemented.
β βββ api/v1/search/home/route.ts -> search
β βββ api/v2/search/home/route.ts -> search
β βββ api/v2/notifications/policy/route.ts-> Feature not implemented.
β βββ 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 -> Website Signup
β βββ api/auth/login/route.ts -> Website Login
β βββ api/auth/logout/route.ts -> Website 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
β β βββ pinafore/TimelineList.tsx -> Theme 1, TimelineList
β β βββ 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
π ν μ΄λΈ μμ / Modify Table
βοΈ user ν
μ΄λΈ – λ²κ·Έμμ μ©
user table – for bug fixes
# 컬λΌμΆκ°
ALTER TABLE users ADD COLUMN mastodon_id TEXT;
UPDATE users SET mastodon_id = lower(hex(randomblob(4))) WHERE mastodon_id IS NULL;
# μ€ν€λ§
sqlite> .schema users
CREATE TABLE users (
username TEXT PRIMARY KEY,
display_name TEXT DEFAULT '',
summary TEXT DEFAULT '',
private_key TEXT,
public_key TEXT,
email TEXT UNIQUE,
password_hash TEXT,
email_verified INTEGER DEFAULT 0,
verification_token TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
, mastodon_id TEXT);
sqlite>
βοΈ posts, inbox_posts ν
μ΄λΈμ reply κΈ°λ₯ μΆκ° / Added reply functionality to the posts and inbox_posts tables.
— in_reply_to_id , in_reply_to_username
# ν
μ΄λΈ μμ - ν°λ―Έλμμ μ€ν / Modify table β Run in the terminal
ALTER TABLE posts ADD COLUMN in_reply_to_id TEXT;
ALTER TABLE posts ADD COLUMN in_reply_to_username TEXT;
ALTER TABLE inbox_posts ADD COLUMN in_reply_to_id TEXT;
ALTER TABLE inbox_posts ADD COLUMN in_reply_to_username TEXT;
ALTER TABLE posts ADD COLUMN in_reply_to_account_id TEXT;
ALTER TABLE inbox_posts ADD COLUMN in_reply_to_account_id TEXT;
CREATE INDEX idx_posts_reply ON posts(in_reply_to_id);
CREATE INDEX idx_inbox_reply ON inbox_posts(in_reply_to_id);
CREATE INDEX idx_posts_reply_account ON posts(in_reply_to_account_id);
CREATE INDEX idx_inbox_reply_account ON inbox_posts(in_reply_to_account_id);
.schema posts
#----------------
# μ€νκ²°κ³Ό / Execution Result
sqlite> .schema posts
CREATE TABLE posts (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
, username TEXT DEFAULT 'user1', visibility TEXT DEFAULT 'public' CHECK(visibility IN ('public','unlisted','private','direct')), original_id TEXT, sensitive INTEGER DEFAULT 0, in_reply_to_id TEXT, in_reply_to_username TEXT);
CREATE INDEX idx_posts_username_created ON posts(username, created_at DESC);
CREATE INDEX idx_posts_visibility ON posts(visibility);
CREATE INDEX idx_posts_reply ON posts(in_reply_to_id);
sqlite> .schema inbox_posts
CREATE TABLE inbox_posts (
id TEXT PRIMARY KEY,
actor TEXT,
content TEXT,
username TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
, original_id TEXT, visibility TEXT DEFAULT 'public' CHECK(visibility IN ('public','unlisted','private','direct')), type TEXT DEFAULT 'Note', sensitive INTEGER DEFAULT 0, in_reply_to_id TEXT, in_reply_to_username TEXT);
CREATE INDEX idx_inbox_username_created ON inbox_posts(username, created_at DESC);
CREATE INDEX idx_inbox_actor ON inbox_posts(actor);
CREATE INDEX idx_inbox_visibility ON inbox_posts(visibility);
CREATE INDEX idx_inbox_reply ON inbox_posts(in_reply_to_id);
π λ²κ·Έμμ / Bug fix
βοΈ api/v1/statuses/route.ts
— APIμμ account : { id: '1' ? '2' … } νλμ½λ© λΆλΆ μ κ±° -> mastodon_idλ‘ μμ
Removed the hardcoded account: { id: '1' ? '2' ... } section in the API and updated it to use mastodon_id.
— λͺ¨λ κΈμ idκ° 1λ²κ³Ό 2λ² λ°μ μλ λ¬Έμ λ°μ
An issue arose where the IDs for all posts were limited to 1 and 2.
account: {
//id: username === 'user1' ? '1' : '2',
id: mastodonId, // β
myapp57
username: username,
acct: `${username}@${DOMAIN}`,
display_name: username,
avatar: `https://${DOMAIN}/icon.png`
},
βοΈ lib/auth.ts
— mastodon_id μ»¬λΌ μΆκ° / Add the mastodon_id column.
— randomUUID().slice(0,8) λ‘ κ³ μ id μμ±
Generate a unique ID using randomUUID().slice(0,8)
export function createUser(username: string, email: string, passwordHash: string, displayName: string = '') {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const userKeyDir = path.join(process.cwd(), `data/keys/${username}`);
if (!fs.existsSync(userKeyDir)) fs.mkdirSync(userKeyDir, { recursive: true });
fs.writeFileSync(path.join(userKeyDir, 'private.pem'), privateKey);
fs.writeFileSync(path.join(userKeyDir, 'public.pem'), publicKey);
console.log(`β
ν€ νμΌ μ μ₯/save key file: data/keys/${username}/`);
// return db.prepare(`
// INSERT INTO users (username, display_name, summary, private_key, public_key, email, password_hash, email_verified, created_at)
// VALUES (?,?, '',?,?,?,?, 1, CURRENT_TIMESTAMP)
// `).run(username, displayName || username, privateKey, publicKey, email, passwordHash);
// β
myapp58 - mastodon_id μμ± / create mastodon_id
const mastodonId = crypto.randomUUID().slice(0, 8);
return db.prepare(`
INSERT INTO users (username, display_name, summary, private_key, public_key, email, password_hash, email_verified, mastodon_id, created_at)
VALUES (?,?, '',?,?,?,?, 1, ?, CURRENT_TIMESTAMP)
`).run(username, displayName || username, privateKey, publicKey, email, passwordHash, mastodonId);
}
βοΈ app/api/v1/timelines/home/route.ts
export function createUser(username: string, email: string, passwordHash: string, displayName: string = '') {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const userKeyDir = path.join(process.cwd(), `data/keys/${username}`);
if (!fs.existsSync(userKeyDir)) fs.mkdirSync(userKeyDir, { recursive: true });
fs.writeFileSync(path.join(userKeyDir, 'private.pem'), privateKey);
fs.writeFileSync(path.join(userKeyDir, 'public.pem'), publicKey);
console.log(`β
ν€ νμΌ μ μ₯/save key file: data/keys/${username}/`);
// return db.prepare(`
// INSERT INTO users (username, display_name, summary, private_key, public_key, email, password_hash, email_verified, created_at)
// VALUES (?,?, '',?,?,?,?, 1, CURRENT_TIMESTAMP)
// `).run(username, displayName || username, privateKey, publicKey, email, passwordHash);
// β
myapp58 - mastodon_id μμ± / create mastodon_id
const mastodonId = crypto.randomUUID().slice(0, 8);
return db.prepare(`
INSERT INTO users (username, display_name, summary, private_key, public_key, email, password_hash, email_verified, mastodon_id, created_at)
VALUES (?,?, '',?,?,?,?, 1, ?, CURRENT_TIMESTAMP)
`).run(username, displayName || username, privateKey, publicKey, email, passwordHash, mastodonId);
}
βοΈ app/api/v1/accounts/verify_credentials/route.ts
π‘ verify_credentials/route.ts λΌμ°νΈμ v1/statuses/[id]/route.tsμ idκ° κ°μμΌ pinaforeμμ delete λ©λ΄λ₯Ό λ³Ό μ μμ.
The id in verify_credentials/route.ts and v1/statuses/[id]/route.ts must match for the delete menu to appear in Pinafore.
... ...
// β
myapp57 - getUserν¨μλ‘ κ³μ νμΈ / Verify the account using the `getUser` function.
function getUser(req: Request) {
const auth = req.headers.get('Authorization')?.replace('Bearer ','').trim();
if (auth) {
// 1. oauth_tokensμμ access_token κ²μ
// Retrieve access_token from oauth_tokens
const o = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(auth) as any;
if (o?.username) return o.username;
// 2. sessionsμμ refresh_tokenλ κ²μ
// Also search for refresh_token in sessions
const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(auth) as any;
if (s?.username) return s.username;
}
const cookie = req.headers.get('cookie') || '';
const m = cookie.match(/refresh_token=([^;]+)/);
if (m) {
const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(m[1]) as any;
if (s?.username) return s.username;
}
return null;
}
export async function GET(req: Request) {
// β
Mastodon νμμΌλ‘ λ¦¬ν΄ / Return in Mastodon format
return NextResponse.json({
id: mastodonId, // β
myapp57
username: username,
acct: `${username}@${DOMAIN}`,
display_name: username,
locked: false,
bot: false,
created_at: new Date().toISOString(),
note: 'My ActivityPub server!',
url: `https://${DOMAIN}/users/${username}`,
avatar: `https://${DOMAIN}/icon.png`,
avatar_static: `https://${DOMAIN}/icon.png`,
header: `https://${DOMAIN}/icon.png`,
header_static: `https://${DOMAIN}/icon.png`,
followers_count: 1,
following_count: 1,
statuses_count: 100,
source: {
privacy: 'public',
sensitive: false,
language: 'en',
note: '',
fields: []
}
});
}
βοΈ usersν
μ΄λΈμ mastodon_idμΈ λΆλΆμ λ°μ΄ν° μ
λ°μ΄νΈ μ€ν
Execute a data update for the mastodon_id field in the users table.
sqlite3 data.sqlite "UPDATE users SET mastodon_id = lower(hex(randomblob(4))) WHERE mastodon_id IS NULL;"
βοΈ app/api/v1/timelines/home/route.ts
// β
myapp48 - visibility κ³΅ν΅ ν¬νΌ import μΆκ° / Added import for common visibility helper
import { getViewerFromRequest, canSee } from '@/lib/visibility';
... ...
export async function GET(req: Request) {
... ...
// β
myapp57 - λ΄ mastodon_id μ‘°ν / select my mastodoin_id
const me = db.prepare('SELECT mastodon_id FROM users WHERE username=?').get(viewer) as any;
const myMastodonId = me?.mastodon_id || viewer;
... ...
// β
myapp55 + myapp57
if (filteredRows.length === 0) {
// const fallback = db.prepare(`SELECT * FROM posts ORDER BY created_at DESC LIMIT 20`).all() as any[];
// console.log(`HOME fallback=${fallback.length}`);
const fallback = db.prepare(`
SELECT * FROM posts
WHERE visibility IN ('public','unlisted')
ORDER BY created_at DESC LIMIT 20
`).all() as any[];
filteredRows = fallback;
}
... ...
// β
myapp57 - κ³μ μ 보 mastodon_idλ‘ μ€μ / Set account information to mastodon_id
const isMy = myPosts.some(p => p.id === row.id);
let accountId = myMastodonId;
let accountUsername = viewer;
... ...
const baseAccount = isMy
? {
id: accountId, //β
myapp57
username, acct: `${username}@${DOMAIN}`, display_name: username,
avatar: `https://${DOMAIN}/icon.png`,
avatar_static: `https://${DOMAIN}/icon.png`,
header: `https://${DOMAIN}/header.png`,
header_static: `https://${DOMAIN}/header.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: row.visibility || 'public', // β
myapp57
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,
// β
myapp55 - Tusky νμ / Required for Tusky
sensitive: false, spoiler_text: '',
media_attachments: [],
mentions: [],
tags: [],
emojis: [],
card: null,
poll: null,
reblog: null,
};
}
π μ½λ μμ / Code Modification
βοΈ λ°μ΄ν°λ² μ΄μ€κ° μμ λμκΈ° λλ¬Έμ κ΄λ ¨λ λΆλΆμ λͺ¨λ μμ ν©λλ€.
Since the database has been modified, all related parts are being updated.
βοΈ app/api/v1/statuses/route.ts – κΈμ°κΈ° API / Writin post API
... ...
export async function POST(req: Request) {
... ...
// β
myapp57 - Add Reply
const inReplyToId = body.in_reply_to_id || body.inReplyToId || null;
const inReplyToUsername = body.in_reply_to_username || body.inReplyToUsername || null;
... ...
// β
myapp57 - posts μ μ₯μ reply μ»¬λΌ μΆκ° / Added reply column to posts storage
try {
db.prepare(`INSERT INTO posts (id, content, created_at, username, visibility, in_reply_to_id, in_reply_to_username) VALUES (?,?,?,?,?,?,?)`)
.run(id, content, now, username, visibility, inReplyToId, inReplyToUsername);
} catch (e) {
console.error('posts μ μ₯ μλ¬ / posts save error', e);
}
... ...
// β
myapp48 + myapp51 + myapp57
// - anyλ‘ ν΄μΌ inReplyTo μΆκ° κ°λ₯ / you need to add "any" type so then you can to use "ReplyTo"
const note: any = {
id: `https://${DOMAIN}/users/${username}/statuses/${id}`,
type: 'Note',
content: `<p>${content}</p>`,
attributedTo: `https://${DOMAIN}/users/${username}`,
published: new Date(now).toISOString(),
to,
cc,
attachment: attachments.map(m => ({
type: 'Document',
mediaType: m.type,
url: toAbsoluteUrl(m.url)
}))
};
}
βοΈ app/api/v1/statuses/[id]/route.ts
— κΈ°μ‘΄ λΌμ°νΈμ GET λΌμ°νΈ μΆκ° / Add a GET route to the existing route.
// β
myapp57 - Pinaforeμμ λ΄ κΈμ΄λ©΄ DELET E보μ¬μ€ / In Pinafore, it shows "DELETE" for my own posts.
export async function GET(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const row = db.prepare(`
SELECT p.*, u.mastodon_id FROM posts p
LEFT JOIN users u ON p.username=u.username
WHERE p.id=?
`).get(id) as any;
if (!row) {
return NextResponse.json({ error: 'not found' }, { status: 404, headers: { 'Access-Control-Allow-Origin': '*' } });
}
return NextResponse.json({
id: String(row.id),
uri: `https://${DOMAIN}/users/${row.username}/statuses/${row.id}`,
url: `https://${DOMAIN}/users/${row.username}/statuses/${row.id}`,
created_at: new Date(row.created_at).toISOString(),
content: row.content?.startsWith('<') ? row.content : `<p>${row.content}</p>`,
account: {
id: String(row.mastodon_id || row.username), // β
μ¬κΈ°κ° verify_credentialsλ κ°μμΌ Delete μκΉ / The 'Delete' option only appears if this matches `verify_credentials`.
username: row.username,
acct: `${row.username}@${DOMAIN}`,
display_name: row.username,
avatar: `https://${DOMAIN}/icon.png`,
avatar_static: `https://${DOMAIN}/icon.png`,
},
visibility: row.visibility || 'public',
in_reply_to_id: row.in_reply_to_id || null,
reblogs_count: 0, favourites_count: 0, replies_count: 0,
favourited: false, reblogged: false, muted: false,
sensitive: false, spoiler_text: '',
media_attachments: [],
mentions: [], tags: [], emojis: [],
}, { headers: { 'Access-Control-Allow-Origin': '*' } });
}
βοΈ app/api/v1/statuses/[id]/context/route.ts – λΌμ°νΈ μΆκ° / Add route – Pinafore
... ...
// β
myapp57 - Pinaforeμμ λ΄ κΈμ΄λ©΄ DELET E보μ¬μ€ / In Pinafore, it shows "DELETE" for my own posts.
export async function GET(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const row = db.prepare(`
SELECT p.*, u.mastodon_id FROM posts p
LEFT JOIN users u ON p.username=u.username
WHERE p.id=?
`).get(id) as any;
if (!row) {
return NextResponse.json({ error: 'not found' }, { status: 404, headers: { 'Access-Control-Allow-Origin': '*' } });
}
return NextResponse.json({
id: String(row.id),
uri: `https://${DOMAIN}/users/${row.username}/statuses/${row.id}`,
url: `https://${DOMAIN}/users/${row.username}/statuses/${row.id}`,
created_at: new Date(row.created_at).toISOString(),
content: row.content?.startsWith('<') ? row.content : `<p>${row.content}</p>`,
account: {
id: String(row.mastodon_id || row.username), // β
μ¬κΈ°κ° verify_credentialsλ κ°μμΌ Delete μκΉ / The 'Delete' option only appears if this matches `verify_credentials`.
username: row.username,
acct: `${row.username}@${DOMAIN}`,
display_name: row.username,
avatar: `https://${DOMAIN}/icon.png`,
avatar_static: `https://${DOMAIN}/icon.png`,
},
visibility: row.visibility || 'public',
in_reply_to_id: row.in_reply_to_id || null,
reblogs_count: 0, favourites_count: 0, replies_count: 0,
favourited: false, reblogged: false, muted: false,
sensitive: false, spoiler_text: '',
media_attachments: [],
mentions: [], tags: [], emojis: [],
}, { headers: { 'Access-Control-Allow-Origin': '*' } });
}
... ...
βοΈ app/api/v1/statuses/[id]/route.ts
export async function GET(req: Request) { ... ...
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: row.visibility || 'public', // β
myapp57
// β
myapp57 - reply νλ μΆκ° / Add reply fileds
in_reply_to_id: row.in_reply_to_id ? String(row.in_reply_to_id) : null,
in_reply_to_account_id: row.in_reply_to_account_id ? String(row.in_reply_to_account_id) : null,
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,
// β
myapp55 - Tusky νμ / Required for Tusky
sensitive: false, spoiler_text: '',
media_attachments: [],
mentions: [],
tags: [],
emojis: [],
card: null,
poll: null,
reblog: null,
};
... ... }
βοΈ app/api/v1/timelines/home/route.ts
... ...
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: row.visibility || 'public', // β
myapp57
// β
myapp57 - reply νλ μΆκ° / Add reply fileds
in_reply_to_id: row.in_reply_to_id ? String(row.in_reply_to_id) : null,
in_reply_to_account_id: row.in_reply_to_account_id ? String(row.in_reply_to_account_id) : null,
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,
// β
myapp55 - Tusky νμ / Required for Tusky
sensitive: false, spoiler_text: '',
media_attachments: (() => {
// β
myapp57 - image μμΌλ©΄ μ¬κΈ°μ λ°ν / Return the image here if it exists.
try {
const medias = db.prepare(`
SELECT ma.* FROM media_attachments ma
JOIN post_media pm ON pm.media_id = ma.id
WHERE pm.post_id=?
`).all(row.id) as any[];
return medias.map((m:any) => ({
id: String(m.id),
type: 'image',
url: m.url.startsWith('http') ? m.url : `https://${DOMAIN}${m.url}`,
preview_url: m.url.startsWith('http') ? m.url : `https://${DOMAIN}${m.url}`,
}));
} catch { return []; }
})(),
mentions: [],
tags: [],
emojis: [],
card: null,
poll: null,
reblog: null,
};
... ...
βοΈ app/api/timeline/route.ts – λ€μ νλ‘μ νΈμμ μμ / Revise in the next project
π ν μ€νΈ / Test
βοΈ λ΅κΈ μμ± / Write a reply

βοΈ λ΅κΈ νμΈ / Check for replies

λ§ν볡μ 21μ₯ 22μ / Matthew 21: 22
“λν¬κ° κΈ°λν λμ 무μμ΄λ μ§ λ―Ώκ³ κ΅¬νλ κ²μ λ€ λ°μΌλ¦¬λΌ νμλλΌ”
“And everything you ask in prayer, believing, you will receive.”