[nextjs]SNS Server-15(myapp25)

๐Ÿ‘‰๐Ÿป myapp25์—์„œ๋Š” ๋ถ€์ŠคํŠธํ•˜๋Š” ๊ฒฝ์šฐ ๋‚ด ํŒ”๋กœ์›Œ์—๊ฒŒ ๊ธ€์„ ์ „๋‹ฌํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
In myapp25, we are implementing a feature that delivers posts to your followers when you “boost” them.

๐Ÿ“ ์ „์ฒด ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ / Overall Project Structure

myapp24/  
โ”œโ”€โ”€ 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
โ”‚   โ”œโ”€โ”€ 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
โ”‚   โ”œโ”€โ”€ layout.tsx, page.tsx, globals.css
โ”‚   โ””โ”€โ”€ favicon.ico
โ”œโ”€โ”€ lib/
โ”‚   โ”œโ”€โ”€ ap.ts                  -> Follow Accept
โ”‚   โ””โ”€โ”€ db.ts                  -> DB connection
โ”œโ”€โ”€ data/
โ”‚   โ””โ”€โ”€ keys/                  -> private.pem, public.pem
โ”œโ”€โ”€ data.sqlite                -> Database
โ”œโ”€โ”€ Caddyfile                  -> https 
โ””โ”€โ”€ package.json

๐Ÿ“ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘(myapp25)
Project Start (myapp25)

npx create-next-app@latest

๐Ÿ“ SQLite์„ค์น˜ / Installing SQLite

cd ~/myapp25
npm install better-sqlite3
npm install -D @types/better-sqlite3

๐Ÿ“ DDNS,https์„ค์ • / DDNS,https settings

๐Ÿ“ ๋„๋ฉ”์ธํ—ˆ์šฉ, @userid ์‚ฌ์šฉํ•˜๊ธฐ
Allowing domains, using @userid

— next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* allow domain*/
  allowedDevOrigins: ['aloy-horizon.duckdns.org', '*.duckdns.org'],

  /* host.domain.org/@userid */
  async rewrites() {
    return [
      {
        source: '/@:username',
        destination: '/usersui/:username',
      },
    ]
  }
};


export default nextConfig;

๐Ÿ“ ๋ถ€์ŠคํŠธ ๋ณด๋‚ผ ๋•Œ / When sending a boost

โœ”๏ธ myapp25/app/api/announce/route.ts

    if (result.ok) {
      db.prepare('INSERT INTO announces (id, username, object) VALUES (?, ?, ?)').run(result.announceDoc.id, username, apObjectId);
    
      // โœ… myapp25 - ํŒ”๋กœ์›Œ์—๊ฒŒ ๋ถ€์ŠคํŠธ ๋ฐฐ๋‹ฌ / Deliver Boosts to followers
      const followers = db.prepare('SELECT inbox, actor FROM followers WHERE username = ?').all(username) as any[];
      console.log(`๐Ÿ“ข ๋ถ€์ŠคํŠธ ๋ฐฐ๋‹ฌ ์‹œ์ž‘: ${followers.length}๋ช…์—๊ฒŒ`);

      for (const f of followers) {
        try {
          // ํŒ”๋กœ์›Œ inbox์— Announce ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ
          // ap.ts์— sendAnnounce ์žฌ์‚ฌ์šฉ
          await sendAnnounce(f.inbox, apObjectId, username);
        } catch (e) {
          console.error(`๋ฐฐ๋‹ฌ ์‹คํŒจ -> ${f.actor}`, e);
        }
      }
      
    }

๐Ÿ“ ๋ถ€์ŠคํŠธ ๋ฐ›์„ ๋•Œ / When receiving a boost

โœ”๏ธ myapp25/app/users/[username]/inbox/route.ts

— ๋ถ€์ŠคํŠธ ๋ฐ›๊ธฐ / Receive boost

 // โœ… myapp25 - ๋ถ€์ŠคํŠธ ๋ฐ›๊ธฐ / Receive Boost !
    if (body.type === 'Announce') {
      try {
        const announceId = body.id;
        const objectId = typeof body.object === 'string' ? body.object : body.object?.id;
        console.log(`๐Ÿ” [${username}] Announce ๋„์ฐฉ: ${actorId} -> ${objectId}`);

        // 1. announces ํ…Œ์ด๋ธ”์— ๊ธฐ๋ก
        // Record in announces table
        db.prepare('INSERT OR IGNORE INTO announces (id, username, object) VALUES (?,?,?)')
          .run(announceId, username, objectId);

        // 2. ์›๊ธ€ ๋‚ด์šฉ ๊ฐ€์ ธ์™€์„œ inbox_posts์— ์ €์žฅ (ํƒ€์ž„๋ผ์ธ์— ๋œจ๊ฒŒ)
        // Retrieve the original post content and save it to inbox_posts (so it appears on the timeline)
        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()}`; // ๋ถ€์ŠคํŠธ๋Š” ๋ณ„๋„ id
            const content = note.content || note.summary || '';

            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());
            console.log(`โœ… [${username}] ๋ถ€์ŠคํŠธ inbox_posts ์ €์žฅ / Boost inbox_posts save: ${shortId}`);
          }
        } catch (fetchErr) {
          console.log(`โš  ์›๊ธ€ fetch ์‹คํŒจ, content ์—†์ด ์ €์žฅ`, fetchErr);
          // fetch ์‹คํŒจํ•ด๋„ ๋ถ€์ŠคํŠธ ๊ธฐ๋ก์€ ๋‚จ๊น€ / Boost records are saved even if fetch fails.
          db.prepare(`
            INSERT OR IGNORE INTO inbox_posts (id, actor, content, username, original_id, created_at) 
            VALUES (?, ?, ?, ?, ?, ?)
          `).run(`boost_${Date.now()}`, actorId, `[Boost] ${objectId}`, username, objectId, new Date().toISOString());
        }

        console.log(`โœ… [${username}] announces ์ €์žฅ ์™„๋ฃŒ / Announcements saved.: ${announceId}`);
      } catch (e) {
        console.error(`โŒ Announce ์ €์žฅ ์‹คํŒจ / Failed to save Announce.`, e);
      }
      return new Response('', { status: 202 });
    }

— ๋ถ€์ŠคํŠธ ์ทจ์†Œ๋ฐ›๊ธฐ / Cancel Boost(Undo Announce)

    // โœ… myapp25 - Undo Announce (๋ถ€์ŠคํŠธ ์ทจ์†Œ ๋ฐ›๊ธฐ)
    if (body.type === 'Undo') {
      // ์œ„์—์„œ ์ด๋ฏธ Follow, Like Undo ์ฒ˜๋ฆฌํ–ˆ์œผ๋‹ˆ๊นŒ ์—ฌ๊ธฐ์„œ๋Š” Announce Undo๋งŒ
      const obj = body.object;
      if (typeof obj === 'object' && obj.type === 'Announce') {
        const announceId = obj.id;
        const objectId = typeof obj.object === 'string' ? obj.object : obj.object?.id;
        console.log(`โ†ฉ [${username}] Undo Announce: ${actorId} -> ${objectId}`);
        
        db.prepare('DELETE FROM announces WHERE id = ?').run(announceId);
        // inbox_posts์—์„œ ๋ถ€์ŠคํŠธ ๊ธ€๋„ ์‚ญ์ œ (์„ ํƒ)
        db.prepare('DELETE FROM inbox_posts WHERE original_id = ? AND actor = ?').run(objectId, actorId);
        console.log(`๐Ÿ—‘ [${username}] ๋ถ€์ŠคํŠธ ์ทจ์†Œ ์ฒ˜๋ฆฌ ์™„๋ฃŒ / Boost processing cancellation complete.`);
        return new Response('', { status: 202 });
      }
    }

๐Ÿ“ ํ…Œ์ŠคํŠธ / Test

# ํŒ”๋กœ์šฐ (following 1๋กœ ์ฆ๊ฐ€)
# Follow (following increased to 1)

curl -X POST https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://mastodon.social/users/mcnees"}'

# ์–ธํŒ”๋กœ์šฐ (following 0์œผ๋กœ ๊ฐ์†Œ)
# Unfollow (following count reduced to 0)

curl -X DELETE https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://mastodon.social/users/mcnees"}'

# ๋ถ€์ŠคํŠธ ์‹คํ–‰ / Boost
curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089019419350090"}'

# ๋ถ€์ŠคํŠธ ์ทจ์†Œ / Cancle Boost
curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089019419350090"}'

โœ”๏ธ ๋งˆ์Šคํ† ๋ˆ ์„œ๋ฒ„์˜ ๊ธ€ ๋ถ€์ŠคํŠธํ•˜๊ธฐ / Boost a post on a Mastodon server

https://mastodon.social/@mcnees/117089019419350090

— ํŒ”๋กœ์šฐ,๋ถ€์ŠคํŠธ์‹คํ–‰ / Execute Follow and Boost

# ๋งˆ์Šคํ† ๋ˆ ์„œ๋ฒ„์— ํŒ”๋กœ์šฐ ์š”์ฒญ / Follow request to a Mastodon server

myapp25 % curl -X POST https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://mastodon.social/users/mcnees"}'

{"ok":true,"inbox":"https://mastodon.social/users/mcnees/inbox","target":"https://mastodon.social/users/mcnees","username":"user1","result":"","follow":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/follows/1787878537250","type":"Follow","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/mcnees"}}%                                                               

# ๋ถ€์ŠคํŠธ ์‹คํ–‰ / Run Boost

 myapp25 % curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089019419350090"}'

{"ok":true,"result":{"ok":true,"status":202,"text":"","announceDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/announces/15aad35a-32de-4de0-b832-9134498b94d5","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/mcnees/statuses/117089019419350090","published":"2026-08-28T00:56:17.391Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers","https://aloy-horizon.duckdns.org/users/user1"]}}}%                                         
gimdaegyeong@gimdaegyeong-ui-MacBookAir myapp25 % 
Mastodon Boost

— ๋‚ด DB์— ๋ถ€์ŠคํŠธ ์ „๋‹ฌํ™•์ธ / Check Boost delivery status in my DB

sqlite> select * from announces;
https://freelifemakers.com/users/user1/statuses/01M12WRSDN77HCBMPES6NH8A4Z|user1|https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG|2026-08-28 00:36:59
https://aloy-horizon.duckdns.org/users/user1/announces/9d4b9bc4-fa12-4073-b034-794c0dde40f8|user1|https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7|2026-08-28 00:46:46
https://aloy-horizon.duckdns.org/users/user1/announces/15aad35a-32de-4de0-b832-9134498b94d5|user1|https://mastodon.social/users/mcnees/statuses/117089019419350090|2026-08-28 00:56:17
sqlite> 

โœ”๏ธ freelifemakers.com์—์„œ ๋ถ€์ŠคํŠธ ํ•œ ๊ฒฝ์šฐ
If you boosted via freelifemakers.com

Screenshot
sqlite> select * from announces;
https://freelifemakers.com/users/user1/statuses/01M12WRSDN77HCBMPES6NH8A4Z|user1|https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG|2026-08-28 00:36:59

โœ”๏ธ aloy-horizon.duckdns.org๊ฐ€ freelifemakers.com์„ ๋ถ€์ŠคํŠธํ•œ ๊ฒฝ์šฐ
If aloy-horizon.duckdns.org has boosted freelifemakers.com

# ๋ถ€์ŠคํŠธ ์‹คํ–‰ / Boost
curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d 
'{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7"}'

# ๋ถ€์ŠคํŠธ ์ทจ์†Œ / Cancle Boost
curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d 
'{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7"}'
myapp25 % curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7"}'

{"ok":true,"result":{"ok":true,"status":202,"text":"{\"status\":\"Accepted\"}","announceDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/announces/9d4b9bc4-fa12-4073-b034-794c0dde40f8","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7","published":"2026-08-28T00:46:46.182Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers","https://aloy-horizon.duckdns.org/users/user1"]}}}%      
sqlite> select * from announces;
https://freelifemakers.com/users/user1/statuses/01M12WRSDN77HCBMPES6NH8A4Z|user1|https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG|2026-08-28 00:36:59
https://aloy-horizon.duckdns.org/users/user1/announces/9d4b9bc4-fa12-4073-b034-794c0dde40f8|user1|https://freelifemakers.com/users/user1/statuses/01M12X859S7WNSJJD42NWZS8T7|2026-08-28 00:46:46
sqlite> 

์š”ํ•œ๋ณต์Œ 8์žฅ 32์ ˆ / John 8:32

“๊ทธ๋ฆฌ๊ณ  ๋„ˆํฌ๋Š” ์ง„๋ฆฌ๋ฅผ ์•Œ๊ฒŒ ๋  ๊ฒƒ์ด๋ฉฐ, ์ง„๋ฆฌ๊ฐ€ ๋„ˆํฌ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ํ•  ๊ฒƒ์ด๋‹ค.”

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

Leave a Reply