[nextjs]SNS Server-2(myapp12)

๐Ÿ‘‰๐Ÿป freelifemaker.com(gotosocial)์„œ๋ฒ„์—์„œ ๋‚˜์˜ nextjs์„œ๋ฒ„๋กœ ํŒ”๋กœ์›Œํ•˜๋Š”๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ •์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.
This explains the process of implementing a follower feature from the freelifemaker.com (GoToSocial) server to my Next.js server.

๐Ÿ‘‰๐Ÿป ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์‹ค์ œ๋กœ ๋„๋ฉ”์ธ์ด ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์–ด์•ผํ•ฉ๋‹ˆ๋‹ค.
To perform the test, the domain must actually be connected.

๐Ÿ‘‰๐Ÿป ๊ทธ๋ž˜์„œ ddns๋กœ ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ์„ ํ•ฉ๋‹ˆ๋‹ค.
So, I connect the domain using DDNS.

๐Ÿ‘‰๐Ÿป https๋Š” ์•ž์ „์— ์„ค๋ช…ํ–ˆ๋˜ caddy๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด๋ฉ๋‹ˆ๋‹ค.
For HTTPS, you can use Caddy, which I explained earlier.

๐Ÿ“ Actor,Inbox,Outbox

โœ”๏ธ ActivityPub SNS์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๊ฐ„๋‹จํ•œ ์šฉ์–ด ์ž…๋‹ˆ๋‹ค.
These are simple terms used in ActivityPub-based social networks.

Actor (์‚ฌ๋žŒ) : 
- https://yourhost.domain.org/users/usename
- Actor๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
"Actor" refers to the user.

Inbox (์šฐํŽธํ•จ / mailbox) : 
- ์ผ์ข…์˜ ์šฐํŽธํ•จ์œผ๋กœ ๋‚จ์ด ๋‚˜ํ•œํ…Œ ํŽธ์ง€ ๋ณด๋‚ด๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค.
It is a kind of mailbox where others can send me letters.

Outbox (๋ฐœ์‹ ํ•จ / Outbox) : 
- ๋‚ด๊ฐ€ ์“ด ๊ธ€ ๋ชฉ๋ก์ด ์ €์žฅ๋˜๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค.
This is where the list of posts I have written is stored.

๐Ÿ“ ์ง„ํ–‰๊ณผ์ •์„ ์š”์•ฝํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
The process is summarized below.

1, ํ‚ค ์ƒ์„ฑ / Key Generation

- ActivityPub์€ ๊ธ€์„ ์“ธ๋•Œ ํ‚ค๋กœ ๊ฒ€์ฆ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
With ActivityPub, you must verify using a key when creating a post.

- private key์™€ ํ•จ๊ป˜ ๋ณด๋‚ด๋ฉด ์ƒ๋Œ€๋ฐฉ ์„œ๋ฒ„๊ฐ€ ๋‚ด ์„œ๋ฒ„์— public key ์š”์ฒญํ•ด์„œ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
When sent along with the private key, the recipient's server requests the public key from my server to verify it.

- private key๋Š” ๋น„๊ณต๊ฐœ ํ‚ค,public key๋Š” actor์—๊ฒŒ ๊ณต๊ฐœํ•˜๋Š” ํ‚ค์ž…๋‹ˆ๋‹ค.
A private key is a key that is kept secret, while a public key is a key disclosed to the actor.

2. DB์— followers ํ…Œ์ด๋ธ” ์ถ”๊ฐ€
Add a 'followers' table to the database.

- lib/db.ts์— followers ํ…Œ์ด๋ธ” ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
Add the `followers` table to `lib/db.ts`.

- ๋ˆ„๊ฐ€ ๋‚˜๋ฅผ ํŒ”๋กœ์šฐ ํ–ˆ๋Š”์ง€ inbox ์ฃผ์†Œ ์ €์žฅ์šฉ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
It is used to store the inbox address of the person who followed me.

3.๋ผ์šฐํŠธ ๋งŒ๋“ค๊ธฐ / Creating a Route

- Actor ๊ฒ€์ƒ‰์‹œ ์ฃผ์†Œ ์•Œ๋ ค์ฃผ๊ธฐ
Provide the address when searching for an actor.
/.well-known/webfinger/route.ts 

- ๋‚ด์ด๋ฆ„,inbox,๊ณต๊ฐœํ‚ค ์•Œ๋ ค์ฃผ๊ธฐ
Share my name, inbox, and public key.
/users/[username]/route.ts 

- Follow๋ฐ›๊ธฐ,์ƒ๋Œ€๋ฐฉ ๊ธ€์„ ๋ฐ›๋Š” ์šฐํŽธํ•จ
"Follow" โ€” an inbox for receiving posts from others.
/users/[username]/inbox/route.ts 

- ๋‚ด๊ฐ€ ์“ด ๊ธ€ ๋ชฉ๋ก
List of posts I've written
/users/[username]/outbox/route.ts

4.ํŒ”๋กœ์šฐํ•˜๊ธฐ / Follow

- yourhost.domain.org ์„œ๋ฒ„์‹คํ–‰(๋˜๋Š” ๋ฐฐํฌ)
Run (or deploy) the server on yourhost.domain.org

- GTS(pinafore.social)์—์„œ @user1@yourhost.domain.org ๊ฒ€์ƒ‰
Search for @user1@yourhost.domain.org on GTS(pinafore.social).


5.GTS(Pinafore)์—์„œ followers 1 ๋งŒ๋“ค๊ธฐ
Creating 1 follower in GTS (Pinafore)

- yourhost.domain.org ํŒ”๋กœ์šฐํ•˜๊ธฐ
Follow yourhost.domain.org

- lib/ap.ts์—์„œ private.pem์œผ๋กœ ์„œ๋ช…ํ•ด์„œ Accept๋ฅผ GTS inbox๋กœ POST์ „์†ก ํ•˜๊ธฐ
Sign the "Accept" activity with `private.pem` and send it via POST to the GTS inbox in `lib/ap.ts`.

- GTS 202 Accepted 

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

myapp12/  
โ”œโ”€โ”€ app/  (Next.js App Router)
โ”‚   โ”œโ”€โ”€ .well-known/webfinger/route.ts     -> webfinger
โ”‚   โ”œโ”€โ”€ api/posts/route.ts     -> ๊ธ€ ์“ฐ๊ธฐ API / Writing API
โ”‚   โ”œโ”€โ”€ 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

๐Ÿ“ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ / Create Project (myapp12)

npx create-next-app@latest

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

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

๐Ÿ“ caddy ์„ค์น˜ / Install Caddy

brew install caddy

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

โœ”๏ธ 1. ํ‚ค ์ƒ์„ฑ (ActivityPub์€ ๋ชจ๋“  ์š”์ฒญ์— ์„œ๋ช…์„ ํ•ฉ๋‹ˆ๋‹ค.)
Key Generation (ActivityPub signs all requests.)

cd ~/myapp12

# ํ‚ค ํด๋” / key folder
mkdir -p data/keys
openssl genrsa -out data/keys/private.pem 2048
openssl rsa -in data/keys/private.pem -pubout -out data/keys/public.pem
echo "data/" >> .gitignore

1)ํ‚ค๋Š” ๋‚ด๊ฐ€ ์š”์ฒญ ์ˆ˜๋ฝํ• ๋•Œ privatekey์™€ ํ•จ๊ป˜ ํŒ”๋กœ์›Œ๋ฅผ ์š”์ฒญํ•œ ์ƒ๋Œ€๋ฐฉ ์„œ๋ฒ„์— ๋ณด๋ƒ…๋‹ˆ๋‹ค.
When I accept a request, the key is sentโ€”along with the private keyโ€”to the server of the person who requested to follow.

2)์ƒ๋Œ€๋ฐฉ์ด ์„œ๋ฒ„๊ฐ€ ๋‚ด ์„œ๋ฒ„์—์„œ publickey๋กœ ๋‚ด๊ฐ€ ๋ณด๋‚ธ privatekey์™€ ๋งž๋Š”์ง€ ํ™•์ธํ•ด ๋ด…๋‹ˆ๋‹ค.
The remote server checks whether the private key I sent matches the public key on my server.

โœ”๏ธ 2. DB์— followers ํ…Œ์ด๋ธ” ์ถ”๊ฐ€
Add a ‘followers’ table to the database.

db.exec(`
  CREATE TABLE IF NOT EXISTS followers (
    id TEXT PRIMARY KEY,
    actor TEXT NOT NULL,
    inbox TEXT NOT NULL
  );
`);

โœ”๏ธ 3.๋ผ์šฐํŠธ ๋งŒ๋“ค๊ธฐ / Creating a Route

file 1: app/.well-known/webfinger/route.ts

file 2: app/users/[username]/route.ts

file 3: app/users/[username]/inbox/route.ts 

file 4: app/users/[username]/outbox/route.ts

โœ”๏ธ 4. ํŒ”๋กœ์šฐํ•˜๊ธฐ / Follow

— https://pinafore.social/ ์—์„œ ๋‚ด ์„œ๋ฒ„์˜ ๊ฒ€์ƒ‰์ด ๊ฐ€๋Šฅํ•˜๊ฒŒํ•˜๋ ค๋ฉด webfinger์™€ Actorํ™•์ธ์ด ๊ฐ€๋Šฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
To enable searching for your server on https://pinafore.social/, WebFinger and Actor verification must be supported.

— webfinger ํ…Œ์ŠคํŠธ(user1์ด ์žˆ๋Š” ์ง€ ํ™•์ธํ•˜๊ธฐ)
WebFinger test (check if user1 exists)

# webfinperํ…Œ์ŠคํŠธ / WebFinger test 
myapp12 % curl "http://localhost:3000/.well-known/webfinger?resource=acct:user1@yourhost.domain.org"

# ์‘๋‹ต / Response
{"subject":"acct:user1@yourhost.doman.org","links":[{"rel":"self","type":"application/activity+json","href":"https://yourhost.domain.org/users/user1"}]}% 

— Actorํ™•์ธํ•˜๊ธฐ / Check Actor

#  HTML ๋ง๊ณ  application/activity+json ํ˜•ํƒœ๋กœ ๋œ JSON์œผ๋กœ ์š”์ฒญํ•˜๊ธฐ
Make the request using JSON in the `application/activity+json` format instead of HTML.
myapp12 %  curl -H "Accept: application/activity+json" "http://localhost:3000/users/user1"

# ์‘๋‹ต / Response
{
  "id": "https://aloy-horizon.duckdns.org/users/user1",
  "type": "Person",
  "preferredUsername": "user1",
  "inbox": "https://aloy-horizon.duckdns.org/users/user1/inbox",
  "outbox": "https://aloy-horizon.duckdns.org/users/user1/outbox",
  "publicKey": { "publicKeyPem": "-----BEGIN PUBLIC KEY-----..." }
}
  • id: ๋‚ด ์ฃผ์†Œ / My address
  • type: Person (Bot, Service๋„ ๊ฐ€๋Šฅ / Bots and services are also supported.)
  • preferredUsername: ๋‚ด ์ด๋ฆ„ / My name (user1)
  • inbox: Follow, ๊ธ€ ๋ฐฐ๋‹ฌ ๋ฐ›๋Š” ๊ณณ / Where to receive the writings
  • outbox: ๋‚ด๊ฐ€ ์“ด ๊ธ€,๋‚ด ํƒ€์ž„๋ผ์ธ / My posts, my timeline
  • publicKey: ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ ํ‚ค / Key for verification

๐Ÿ“ ์‹คํ–‰ / Run

— Nextjs์„œ๋ฒ„ ์‹คํ–‰ / Run NextJS Server

npm run dev

— Caddy ์‹คํ–‰

Caddy start

— pinafore.social์—์„œ ๊ฒ€์ƒ‰ํ•ด์„œ ๋‚ด์„œ๋ฒ„๊ฐ€ ๊ฒ€์ƒ‰๋˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณด๊ธฐ
Search on pinafore.social to see if my server shows up in the results.

search

— freelifemakers.com ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์„œ ๊ฒ€์ƒ‰๋œ ๋‚ด sns์„œ๋ฒ„์— ํŒ”๋กœ์šฐํ•˜๊ธฐ
Log in with your freelifemakers.com account and follow the discovered SNS server.

Followers

— pinafore.social์—์„œ ํŒ”๋กœ์šฐ๊ฐ€ ๋˜๊ณ  ํŒ”๋กœ์›Œ๊ฐ€ 1์ด๋˜๋Š” ๋กœ์ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
The logic for gaining a follower on pinafore.social is as follows.


export async function POST(req: Request, { params }: { params: Promise<{ username: string }> }) {
  const { username } = await params;
  try {
    const body = await req.json();
    console.log(`๐Ÿ“ฉ [${username}] INBOX:`, body.type, body.actor);

    // ์ƒ๋Œ€๋ฐฉ publicํ‚ค๋กœ ๊ฒ€์ฆ, ํ…Œ์ŠคํŠธ ๋ถ€๋ถ„์ด๋ผ ์ œ์™ธ ์‹œํ‚ด
    // Verification using the other party's public keyโ€”excluded as it is part of the testing phase.
    // const actorUrl = body.actor;
    // const actorData = await fetch(actorUrl, {
    //   headers: { Accept: 'application/activity+json' }
    // }).then(r => r.json());
    
    // const publicKeyPem = actorData.publicKey.publicKeyPem;
    // const isValid = verify(req, publicKeyPem); 

    // if (!isValid) return Response.json({}, { status: 401 });

    const actorId = typeof body.actor === 'string' ? body.actor : body.actor?.id;
    const actorInbox = typeof body.actor === 'object' ? body.actor?.inbox : null;

    if (body.type === 'Follow') {
      const inboxUrl = actorInbox || `${actorId}/inbox`;
      db.prepare('INSERT OR IGNORE INTO followers (id, actor, inbox) VALUES (?,?,?)')
        .run(body.id, actorId, inboxUrl);
      console.log(`โœ… [${username}] ํŒ”๋กœ์šฐ ์ €์žฅ / follow save : ${actorId}`);

      // Accept ๋น„๋™๊ธฐ๋กœ ์ „์†ก (์‘๋‹ต ๋นจ๋ฆฌ ์ฃผ๋ ค๊ณ ) / Send Accept asynchronously (to respond quickly)
      sendAccept(inboxUrl, body, username).catch(e => console.error('Accept ์‹คํŒจ:', e));
    }

    return new Response('', { status: 202 });
  } catch (e) {
    console.error('inbox ์—๋Ÿฌ / inbox error:', e);
    return new Response('', { status: 202 });
  }
}

1)@user1@yourhost.domain.org ์— ํŒ”๋กœ์šฐ ํ•˜๋Š” ์‚ฌ๋žŒ์˜ ์ •๋ณด๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
Stores information about people following @user1@yourhost.domain.org.

2)freelifemakers.com (GoToSocial ์„œ๋ฒ„)์— ๋‚ด๊ฐ€ ๋ˆ„๊ตด ํŒ”๋กœ์šฐํ•˜๋Š” ๊ฑด์ง€ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
It stores who I am following on the freelifemakers.com (GoToSocial) server.

3)๊ทธ๋ž˜์„œ ์‹ค์ œ๋กœ ํŒ”๋กœ์›Œ ์ˆซ์ž๋Š” freelifemakers.com์„œ๋ฒ„์˜ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒ๋‹ˆ๋‹ค.
So, the actual follower count reflects the information from the freelifemakers.com server.

4) pinafore์—์„œ ํŒ”๋กœ์šฐ ์•„์ด์ฝ˜์ด ๋ฐ”๋€Œ๋Š”๊ฒƒ๋„ freelifemakers.com์„œ๋ฒ„์˜ ์ •๋ณด๋ฅผ ํ†ตํ•ด์„œ ๋ฐ”๋€Œ๋Š” ๊ฒ๋‹ˆ๋‹ค.
The change of the “Follow” icon in Pinafore is also driven by information from the freelifemakers.com server.

5) ๋‚ด์„œ๋ฒ„์— ์ €์žฅํ•˜๋Š” ์ •๋ณด๋Š” ์ƒ๋Œ€๋ฐฉ ์„œ๋ฒ„์— ๊ธ€์„ ๋ฐฐ๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ์ •๋ณด์ž…๋‹ˆ๋‹ค.
The information stored on my server is what is required to deliver the post to the recipient’s server.

4)์œ„์˜ ์ฝ”๋“œ์—์„œ๋Š” public key์š”์ฒญํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ๋น ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.
The code for requesting the public key is missing from the code above.

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

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

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

Leave a Reply