[nextjs]SNS Server-47(myapp57)Β 

πŸ‘‰πŸ» 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

reply

βœ”οΈ λ‹΅κΈ€ 확인 / Check for replies

reply

λ§ˆνƒœλ³΅μŒ 21μž₯ 22절 / Matthew 21: 22

“λ„ˆν¬κ°€ 기도할 λ•Œμ— 무엇이든지 λ―Ώκ³  κ΅¬ν•˜λŠ” 것은 λ‹€ λ°›μœΌλ¦¬λΌ ν•˜μ‹œλ‹ˆλΌ”

“And everything you ask in prayer, believing, you will receive.”

Leave a Reply