[nextjs]SNS Server-4(myapp14)

๐Ÿ‘‰๐Ÿป myapp13ํ”„๋กœ์ ํŠธ์—์„œ user1@freelifemakers.com์—์„œ user1@yourhost.domain.org๋ฅผ ํŒ”๋กœ์šฐ ํ–ˆ์Šต๋‹ˆ๋‹ค.
In the myapp13 project, user1@freelifemakers.com followed user1@yourhost.domain.org.

๐Ÿ‘‰๐Ÿป ๊ทธ๋ฆฌ๊ณ  user1@yourhost.domain.org๋กœ ๋กœ๊ทธ์ธํ•ด์„œ ๊ธ€์„ ์“ธ ๊ฒฝ์šฐuser1@freelifemakers.com์— ๋ฐฐ๋‹ฌ๋˜๊ณ  pinafore.social์—์„œ๋„ ํ™•์ธ ๊ฐ€๋Šฅํ•œ์ง€ ํ…Œ์ŠคํŠธ ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค.
I also tested whether logging in as user1@yourhost.domain.org and posting a message would result in delivery to user1@freelifemakers.com and visibility on pinafore.social.

๐Ÿ‘‰๐Ÿป ์ด๋ฒˆ myapp14ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐ˜๋Œ€๋กœ user1@yourhost.domain.org๊ฐ€ user1@freelifemakers.com์„ ํŒ”๋กœ์šฐ ํ•ฉ๋‹ˆ๋‹ค.
In this myapp14 project, conversely, user1@yourhost.domain.org follows user1@freelifemakers.com.

๐Ÿ‘‰๐Ÿป ๊ทธ๋ฆฌ๊ณ  user1@freelifemakers.com์—์„œ ๊ธ€์„ ์“ธ ๊ฒฝ์šฐ user1@yourhost.domain.org๋กœ ๊ธ€์ด ๋ฐฐ๋‹ฌ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
Also, verify whether messages sent from user1@freelifemakers.com are delivered to user1@yourhost.domain.org.


๐Ÿ“ ํ”„๋กœ์ ํŠธ ์„ค์น˜(myapp14) ,HTTPS์„ค์ •
Project Installation (myapp14), HTTPS Configuration

โœ”๏ธ nextjsํ”„๋กœ์ ํŠธ์„ค์น˜,sqlite์„ค์น˜,Caddy์„ค์น˜ ๋ฐ ์„ค์ •
Next.js project setup, SQLite installation, and Caddy installation and configuration

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

myapp14/  
โ”œโ”€โ”€ app/  (Next.js App Router)
โ”‚   โ”œโ”€โ”€ .well-known/webfinger/route.ts     -> webfinger
โ”‚   โ”œโ”€โ”€ api/posts/route.ts     -> ๊ธ€ ์“ฐ๊ธฐ API / Writing API
โ”‚   โ”œโ”€โ”€ api/follow/route.ts    -> ํŒ”๋กœ์šฐ API(์ž„์‹œ) / Follow API(temporary)
โ”‚   โ”œโ”€โ”€ users/[username]/
โ”‚   โ”‚   โ”œโ”€โ”€ route.ts           -> Actor์ •๋ณด / Acotr Information
โ”‚   โ”‚   โ”œโ”€โ”€ inbox/route.ts     -> Inbox
โ”‚   โ”‚   โ””โ”€โ”€ outbox/route.ts    -> outbox
โ”‚   โ”œโ”€โ”€ 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

๐Ÿ“ ์ฝ”๋“œ ์ž‘์„ฑ / Writing code

โœ”๏ธsendFollow ํ•จ์ˆ˜ ์ถ”๊ฐ€ / Add sendFollow function

— myapp14/lib/ap.ts

— ์š”์ฒญ์„ ๋ณด๋‚ผ๋•Œ ๊ทธ๋ƒฅ fetch๋กœ๋ณด๋‚ด๋ฉด gotosocial์ด 401 Unauthorized ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ ์‹œํ‚ต๋‹ˆ๋‹ค.
If you send a request using just fetch, GoToSocial returns a 401 Unauthorized error.

— ๊ทธ๋ž˜์„œ private key๋กœ ์‚ฌ์ธํ•ด์„œ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
So, it is signed with a private key and sent.

— sendFollow๋กœ Follow ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
Send a follow request using sendFollow.

export async function sendFollow(toInbox: string, targetActor: string, username: string) {
  const actorId = `https://${DOMAIN}/users/${username}`;
  const followId = `${actorId}/follows/${Date.now()}`;
  
  const followDoc = {
    '@context': 'https://www.w3.org/ns/activitystreams',
    id: followId,
    type: 'Follow',
    actor: actorId,
    object: targetActor
  };

  const body = JSON.stringify(followDoc);
  const url = new URL(toInbox);
  
  const digest = `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
  const date = new Date().toUTCString();
  const signingString = `(request-target): post ${url.pathname}\nhost: ${url.host}\ndate: ${date}\ndigest: ${digest}`;
  
  const signer = crypto.createSign('sha256');
  signer.update(signingString);
  const signature = signer.sign(PRIVATE_KEY, 'base64');

  const keyId = `${actorId}#main-key`;
  const sigHeader = `keyId="${keyId}",headers="(request-target) host date digest",signature="${signature}"`;

  console.log(`โžก๏ธ [${username}] Follow ์ „์†ก / Follow sent -> ${toInbox} (${targetActor})`);

  const res = await fetch(toInbox, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/activity+json',
      'Date': date,
      'Digest': digest,
      'Signature': sigHeader,
      'Host': url.host
    },
    body
  });

  const text = await res.text();
  console.log(`๐Ÿ“ฌ Follow ๊ฒฐ๊ณผ / Follow result: ${res.status}`, text);
  return { ok: res.ok, status: res.status, text, followDoc };
}

โœ”๏ธ signedFetch ํ•จ์ˆ˜ ์ถ”๊ฐ€
Add signedFetch function

— myapp14/lib/ap.ts

— ์š”์ฒญ์„ ๋ณด๋‚ผ๋•Œ ๊ทธ๋ƒฅ fetch๋กœ๋ณด๋‚ด๋ฉด gotosocial์ด 401 Unauthorized ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ ์‹œํ‚ต๋‹ˆ๋‹ค.
If you send a request using just fetch, GoToSocial returns a 401 Unauthorized error.

— ๊ทธ๋ž˜์„œ private key๋กœ ์‚ฌ์ธํ•ด์„œ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
So, it is signed with a private key and sent.

— signedFetch๋กœ ์ƒ๋Œ€๋ฐฉ inbox ์ฐพ๋Š” ๊ธฐ๋Šฅ
Functionality to locate the recipient’s inbox using signedFetch

export async function signedFetch(urlStr: string, username: string) {
  const actorId = `https://${DOMAIN}/users/${username}`;
  const url = new URL(urlStr);
  const date = new Date().toUTCString();
  const signingString = `(request-target): get ${url.pathname}\nhost: ${url.host}\ndate: ${date}`;
  
  const signer = crypto.createSign('sha256');
  signer.update(signingString);
  const signature = signer.sign(PRIVATE_KEY, 'base64');

  const keyId = `${actorId}#main-key`;
  const sigHeader = `keyId="${keyId}",headers="(request-target) host date",signature="${signature}"`;

  console.log(`๐Ÿ” Signed GET -> ${urlStr}`);

  return fetch(urlStr, {
    headers: {
      'Accept': 'application/activity+json',
      'Date': date,
      'Signature': sigHeader,
      'Host': url.host
    }
  });
}

โœ”๏ธ follow๋ผ์šฐํŠธ ์ถ”๊ฐ€
Add follow route

— myapp14/app/api/follow/route.ts

// myapp14/app/api/follow/route.ts
import { sendFollow, signedFetch } from '@/lib/ap';

// myapp14 - Follow ๋ณด๋‚ด๊ธฐ API โœ…
// POST /api/follow
export async function POST(req: Request) {
  try {
    const { username = 'user1', target } = await req.json();
    if (!target) return Response.json({ error: 'target ํ•„์š”' }, { status: 400 });

    console.log(`โžก๏ธ ํŒ”๋กœ์šฐ ์‹œ๋„ / Follow attempt: ${username} -> ${target}`);

    // 1. ์ƒ๋Œ€๋ฐฉ inbox ์ฐพ๊ธฐ - ์„œ๋ช…๋œ GET์œผ๋กœ! / Find the inbox with a signed GET request
    const actorRes = await signedFetch(target, username);
    
    if (!actorRes.ok) {
      const t = await actorRes.text();
      return Response.json({ error: `์ƒ๋Œ€๋ฐฉ ์กฐํšŒ ์‹คํŒจ / Failed to look up the other party. ${actorRes.status}`, body: t }, { status: 400 });
    }

    const actor = await actorRes.json();
    const inbox = actor.inbox;
    console.log(`๐Ÿ“ฌ inbox ์ฐพ์Œ / Found inbox: ${inbox}`);

    // 2. Follow ์ „์†ก / Send Follow
    const result = await sendFollow(inbox, target, username);

    return Response.json({ 
      ok: result.ok, 
      inbox, 
      target,
      result: result.text,
      follow: result.followDoc
    });

  } catch (e: any) {
    console.error('follow ์—๋Ÿฌ / Follow error:', e);
    return Response.json({ error: e.message }, { status: 500 });
  }
}

๐Ÿ“ ๋‚ด ์„œ๋ฒ„์—์„œ user1@freelifemakers.com๋ฅผ ํŒ”๋กœ์šฐํ•˜๊ธฐ
Follow user1@freelifemakers.com from my server

โœ”๏ธ ํŒ”๋กœ์šฐ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” pinafore์— ๋กœ๊ทธ์ธํ•ด์•ผํ•˜์ง€๋งŒ ์•„์ง ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
You need to log in to Pinafore to follow, but the login functionality hasn’t been implemented yet.

โœ”๏ธ ๊ทธ๋ž˜์„œ ํ„ฐ๋ฏธ๋„์—์„œ curl ๋ช…๋ น์–ด๋กœ ๋‚ด ๋กœ์ปฌ์„œ๋ฒ„(yourhost.domain.org)์—์„œ user1@freelifemakers.com๋ฅผ ํŒ”๋กœ์šฐํ•ฉ๋‹ˆ๋‹ค.
So, I use the curl command in the terminal to follow user1@freelifemakers.com from my local server (yourhost.domain.org).

โœ”๏ธ /follow ๋ผ์šฐํŠธ๊ฐ€ -d ํŒŒ๋ผ๋ฉ”ํ„ฐ์˜ ์ •๋ณด๋กœ ํŒ”๋กœ์šฐ๋ฅผ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
The /follow route attempts to follow using the information from the -d parameter.

# ๋‚ด ํ„ฐ๋ฏธ๋„ / My Terminal

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

{"ok":true,"inbox":"https://freelifemakers.com/users/user1/inbox","target":"https://freelifemakers.com/users/user1","result":"{\"status\":\"Accepted\"}","follow":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://yourhost.domain.org/users/user1/follows/1786763495558","type":"Follow","actor":"https://yourhost.domain.org/users/user1","object":"https://freelifemakers.com/users/user1"}}
# nextjs ์„œ๋ฒ„ ๋กœ๊ทธ / nextjs server log (ok!)

โžก๏ธ [user1] Follow ์ „์†ก -> https://freelifemakers.com/users/user1/inbox (https://freelifemakers.com/users/user1)
๐Ÿ“ฌ Follow ๊ฒฐ๊ณผ: 202 {"status":"Accepted"}

๐Ÿ“ ๊ธ€ ๋ฐฐ๋‹ฌ ํ™•์ธ ํ•˜๊ธฐ / Check for delivered messages

โœ”๏ธ ๊ธ€์ด ๋‚ด ์„œ๋ฒ„๋กœ ๋ฐฐ๋‹ฌ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ์œ„ํ•ด์„œ pinafore.social์—์„œ ๊ธ€์„ ์จ๋ด…๋‹ˆ๋‹ค.
I am writing a post on pinafore.social to check if it gets delivered to my server.

โœ”๏ธ ์ด๋•Œ ๋กœ๊ทธ์ธ์€ user1@freelifemakers.com ์ธ๊ฒฝ์šฐ ์ž…๋‹ˆ๋‹ค.
In this case, the login account is user1@freelifemakers.com.

pinafore

โœ”๏ธ ๊ธ€ ๋ฐฐ๋‹ฌํ™•์ธํ•˜๊ธฐ(๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค)
Check Message Delivery Status (Database)

sqlite> select content from posts;
Hello! my SNS!!
Hello! my SNS!!
[from: https://freelifemakers.com/users/user1] <p>hello! my follow test!!</p>
[from: https://freelifemakers.com/users/user1] <p>hello my follow test</p>
sqlite> 

โœ”๏ธ ์„œ๋ฒ„ ๋กœ๊ทธ / Server logs

๐Ÿ“ฉ [user1] INBOX: Create https://freelifemakers.com/users/user1
๐Ÿ“ [user1] ์ƒˆ ๊ธ€ ๋„์ฐฉ from https://freelifemakers.com/users/user1
๋‚ด์šฉ: <p>hello! my follow test!!</p>
โœ… [user1] ๊ธ€ ์ €์žฅ ์™„๋ฃŒ!
 POST /users/user1/inbox 202 in 26ms (next.js: 15ms, application-code: 11ms)
๐Ÿ“ฉ [user1] INBOX: Create https://freelifemakers.com/users/user1
๐Ÿ“ [user1] ์ƒˆ ๊ธ€ ๋„์ฐฉ from https://freelifemakers.com/users/user1
๋‚ด์šฉ: <p>hello my follow test</p>
โœ… [user1] ๊ธ€ ์ €์žฅ ์™„๋ฃŒ!

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

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

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

Leave a Reply