๐๐ป 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.

โ๏ธ ๊ธ ๋ฐฐ๋ฌํ์ธํ๊ธฐ(๋ฐ์ดํฐ๋ฒ ์ด์ค)
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”