[nextjs]SNS Server-26(myapp36)Β 

πŸ‘‰πŸ» νšŒμ›κ°€μž… 및 둜그인과 κ΄€λ ¨λœ κΈ°λŠ₯μž…λ‹ˆλ‹€.
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의 글을 ν™•μΈν•©λ‹ˆλ‹€.

Pinafore

βœ”οΈ aloy-horizon.duckdns.org/@user1

— λͺ¨λ“  데이터와 μΉ΄μš΄νŠΈλŠ” μ—¬κΈ°μ„œ 확인 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
You can check all the data and counts here.

Local Server

μš”ν•œλ³΅μŒ 8μž₯ 32절 / John 8:32

“그리고 λ„ˆν¬λŠ” 진리λ₯Ό μ•Œκ²Œ 될 것이며, 진리가 λ„ˆν¬λ₯Ό 자유둭게 ν•  것이닀.”

“Then you will know the truth ,and the truth will set you free”

Leave a Reply