๐๐ป 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 addresstype: Person(Bot, Service๋ ๊ฐ๋ฅ / Bots and services are also supported.)preferredUsername: ๋ด ์ด๋ฆ / My name (user1)inbox: Follow, ๊ธ ๋ฐฐ๋ฌ ๋ฐ๋ ๊ณณ / Where to receive the writingsoutbox: ๋ด๊ฐ ์ด ๊ธ,๋ด ํ์๋ผ์ธ / My posts, my timelinepublicKey: ๊ฒ์ฆํ๊ธฐ ์ํ ํค / 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.

— freelifemakers.com ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํด์ ๊ฒ์๋ ๋ด sns์๋ฒ์ ํ๋ก์ฐํ๊ธฐ
Log in with your freelifemakers.com account and follow the discovered SNS server.

— 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”