๐๐ป @user1@aloy-horizon.duckdns.org์ follows์ ๊ฐ์ ์ฌ์ฉ์๊ฐ ๋๋ฒ ์
๋ ฅ๋๋ ๋ฌธ์ ๋ฅผ ์์ ํฉ๋๋ค.
Fixes an issue where the same user is entered twice in the ‘follows’ list for @user1@aloy-horizon.duckdns.org.
๐๐ป follow์์ฒญ์ following๊ณผ followersํ
์ด๋ธ์ ๊ธฐ์กด ์ฌ์ฉ์ ์์ผ๋ฉด ์์ฒญ์ ๋ณด๋ด์ง ์์ต๋๋ค.
When a follow request is made, the request is not sent if the user already exists in the ‘following’ or ‘followers’ tables.
๐ ์ ์ฒด ํ๋ก์ ํธ ๊ตฌ์กฐ / Overall Project Structure
myapp21/
โโโ app/ (Next.js App Router)
โ โโโ .well-known/webfinger/route.ts -> webfinger
โ โโโ api/posts/route.ts -> Writing API
โ โโโ api/follow/route.ts -> Follow API(temporary)
โ โโโ 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
๐ ํ๋ก์ ํธ ์์(myapp21)
Project Start (myapp21)
npx create-next-app@latest
๐ SQLite์ค์น / Installing SQLite
cd ~/myapp20
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;
๐ Following ์ฒ๋ฆฌ ๋ถ๋ถ ์์ / Modification to the ‘Following’ processing logic
โ๏ธ @user1@aloy-horizon.duckdns.org ๊ณ์ ์์ followers๊ฐ ์ค๋ณต ์์ฒญ ํ์ง ์๋๋กํฉ๋๋ค.
Prevent duplicate requests from followers for the @user1@aloy-horizon.duckdns.org account.
— ์ ๋ํฌ ์ธ๋ฑ์ค ์ถ๊ฐ(sqlite) / Add Unique Index (SQLite)
CREATE UNIQUE INDEX IF NOT EXISTS idx_following_actor_username ON following(actor, username);
— myapp21/app/api/follow/route.ts
... ...
export async function POST(req: Request) {
... ...
// myapp21 โ
// ์ด๋ฏธ ํ๋ก์ ์ค์ด๋ฉด ๋คํธ์ํฌ ์์ฒญ์ ๋ณด๋ด์ง ์์
// Do not send a network request if already following
const already = db.prepare('SELECT id FROM following WHERE actor = ? AND username = ?').get(target, username) as any;
if (already) {
console.log(`โน๏ธ [${username}] ์ด๋ฏธ ํ๋ก์ ์ค / Already following - ์์ฒญ ์คํต / Skip request: ${target}`);
return Response.json({
ok: true,
alreadyFollowing: true,
target,
username,
message: '์ด๋ฏธ ํ๋ก์ ์ค / Already following'
});
... ...
}
... ...
// myapp21 โ
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ค๋ณต ์
๋ ฅ ๋ฐฉ์ง / Preventing duplicate entries in the database
if (result.ok) {
try {
db.prepare('INSERT INTO following (id, actor, username) VALUES (?, ?, ?)')
.run(result.followDoc.id, target, username);
console.log(`โ
DB ์ ์ฅ / DB Save: ${username} -> ${target}`);
} catch (e: any) {
if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') {
console.log(`โน๏ธ ์ด๋ฏธ ์ ์ฅ๋จ / Already saved: ${target}`);
} else {
console.error('DB ์ ์ฅ ์คํจ / DB save failed:', e);
}
}
}
... ...
}
... ...
export async function DELETE(req: Request) {
... ...
// โ
myapp21 - ์ธํ๋ก์ฐ ์ค๋ณต ์ฒดํฌ /
const exists = db.prepare('SELECT id FROM following WHERE actor = ? AND username = ?').get(target, username) as any;
if (!exists) {
console.log(`โน๏ธ [${username}] ํ๋ก์ ์ค ์๋ / Not Following - ์ธํ ์คํต / Skip Unfollowing: ${target}`);
return Response.json({ ok: true, alreadyNotFollowing: true, message: '์ด๋ฏธ ์ธํ ์ํ / Already unfollowed.' });
}
... ...
}
— myapp21/lib/db.ts
db.exec(`
CREATE INDEX IF NOT EXISTS idx_followers_username ON followers(username);
CREATE INDEX IF NOT EXISTS idx_followers_actor ON followers(actor);
CREATE INDEX IF NOT EXISTS idx_following_username ON following(username);
CREATE INDEX IF NOT EXISTS idx_following_actor ON following(actor);
CREATE INDEX IF NOT EXISTS idx_posts_username_created ON posts(username, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_inbox_username_created ON inbox_posts(username, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_inbox_actor ON inbox_posts(actor);
CREATE UNIQUE INDEX IF NOT EXISTS idx_following_actor_username ON following(actor, username);
`);
๐ Follower ์ฒ๋ฆฌ ๋ถ๋ถ ์์ / Modified the follower handling logic.
— ์ ๋ํฌ ์ธ๋ฑ์ค ์ถ๊ฐ / Add unique index (sqlite)
CREATE UNIQUE INDEX idx_followers_actor_username ON followers(actor, username);
— myapp21/app/users/[username]/inbox/route.ts
... ...
// โ
myapp21
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ค๋ณต ์
๋ ฅ ๋ฐฉ์ง / Preventing duplicate entries in the database
const exists = db.prepare('SELECT 1 FROM followers WHERE actor = ? AND username = ?').get(actorId, username) as any;
if (exists) {
console.log(`โน๏ธ [${username}] ์ด๋ฏธ ํ๋ก์ / Already follower: ${actorId} - ์ค๋ณต ๋ฌด์/Ignore duplicates`);
} else {
db.prepare('INSERT INTO followers (id, actor, inbox, username) VALUES (?,?,?,?)')
.run(body.id, actorId, inboxUrl, username);
console.log(`โ
[${username}] ํ๋ก์ฐ ์ ์ฅ / follow save : ${actorId}`);
}
— myapp21/lib/db.ts
CREATE UNIQUE INDEX IF NOT EXISTS idx_following_actor_username ON following(actor, username);
CREATE UNIQUE INDEX idx_followers_actor_username ON followers(actor, username);
๐ gotosocial DB(docker compose)
โ๏ธ์๋์ ๊ณผ์ ์ผ๋ก ๊ธฐ์กด DB๋ฅผ ์ด๊ธฐํ ํ ์ ์์ต๋๋ค.
You can initialize the existing database by following the steps below.
# DB ๋ค์ด๊ฐ๊ธฐ / Accessing the DB
sqlite3 sqlite.db
SELECT username, domain, followers_count, following_count FROM accounts WHERE domain='aloy-horizon.duckdns.org';
# ์ญ์ / delete
DELETE FROM accounts WHERE domain='aloy-horizon.duckdns.org' AND username='user1';
.quit
# ์๋ฒ ์ฌ์์ / Server restart
docker compose restart gotosocial
docker compose logs -f gotosocial
โ๏ธ sqlite์์ ์ค๋ฅ๊ฐ ์๋ ๋ฐ์ดํฐ ์ญ์
— ์๋์ฒ๋ผ followers,following,toots๋ฅผ ๋ชจ๋ 0์ผ๋ก ์ด๊ธฐํ ์ํต๋๋ค.
sqlite> SELECT id, username, domain, uri FROM accounts WHERE domain='aloy-horizon.duckdns.org';
01KZWNMF7BFD5N6MAYV3068C81|user1|aloy-horizon.duckdns.org|https://aloy-horizon.duckdns.org/users/user1
sqlite> SELECT * FROM account_stats WHERE account_id='01KZWNMF7BFD5N6MAYV3068C81';
01KZWNMF7BFD5N6MAYV3068C81|2026-06-10 01:20:08.931+00:00|0|1|0|2|0|
-- follows ํ
์ด๋ธ ํ์ธ / Check the 'follows' table.
sqlite>SELECT * FROM follows WHERE account_id='01KZWNMF7BFD5N6MAYV3068C81' OR target_account_id='01KZWNMF7BFD5N6MAYV3068C81';
sqlite>DELETE FROM account_stats WHERE account_id='01KZWNMF7BFD5N6MAYV3068C81';
sqlite>DELETE FROM accounts WHERE id='01KZWNMF7BFD5N6MAYV3068C81';
-- follows๋ ์ด๋ฏธ ๋น์ด์์ / 'follows' is already empty.
-- toots ์ค๋ฅ ์์ด์ ๋ชจ๋ ์ญ์ / Deleted everything due to errors with Toots.
sqlite> SELECT id FROM accounts WHERE domain='aloy-horizon.duckdns.org';
01M0RKD5BF8JEGA7HN2A66Z54C
sqlite> SELECT COUNT(*) FROM statuses WHERE account_id='01M0RKD5BF8JEGA7HN2A66Z54C';
0
sqlite> SELECT COUNT(*) FROM statuses WHERE account_id='01KZWNMF7BFD5N6MAYV3068C81';
3
sqlite> DELETE FROM statuses WHERE account_id='01KZWNMF7BFD5N6MAYV3068C81';
sqlite> DELETE FROM account_stats WHERE account_id='01KZWNMF7BFD5N6MAYV3068C81';
sqlite> DELETE FROM accounts WHERE id='01KZWNMF7BFD5N6MAYV3068C81';
sqlite> SELECT id, username, domain FROM accounts WHERE domain='aloy-horizon.duckdns.org';
01M0RKD5BF8JEGA7HN2A66Z54C|user1|aloy-horizon.duckdns.org
sqlite> DELETE FROM statuses WHERE account_id='01M0RKD5BF8JEGA7HN2A66Z54C';
sqlite> DELETE FROM account_stats WHERE account_id='01M0RKD5BF8JEGA7HN2A66Z54C';
sqlite> DELETE FROM accounts WHERE id='01M0RKD5BF8JEGA7HN2A66Z54C';
sqlite> DELETE FROM statuses WHERE id IN (SELECT id FROM statuses WHERE account_id NOT IN (SELECT id FROM accounts));
sqlite> .quit
-- ๋์ปค ์ปดํฌ์ฆ๋ฅผ ์ฌ์์ํฉ๋๋ค.
docker compose down
docker compose up -d
๐ ํ ์คํธ / Test
โ๏ธ ํฐ๋ฏธ๋์์ ์๋์ ๋ผ์ฐํธ๋ก ํ
์คํธ ํ ์ ์์ต๋๋ค.
You can test using the route below in the terminal.
# ํ๋ก์ฐ (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://freelifemakers.com/users/user1"}'
# ์ธํ๋ก์ฐ (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://freelifemakers.com/users/user1"}'
# ๊ธ ์ฐ๊ธฐ (outbox 7๋ก ์ฆ๊ฐ + ํ๋ก์๋ค์๊ฒ ๋ฐฐ๋ฌ)
# Writing a post (added to outbox 7 + delivered to followers)
curl -X POST https://aloy-horizon.duckdns.org/api/posts \
-H "Content-Type: application/json" \
-d '{"username":"user1","content":"Hello Follow! #test"}'
โ๏ธ ํ๋ก์ฐ ํ ์คํธ / Follow Test
–ํ๋ก์ฐ ์์ฒญ์ ๋๋ฒํ๋ฉด ์์ฒญ์ ๋ณด๋ด์ง ์์ต๋๋ค.
If you send a follow request twice, the request will not be sent.
~ % 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","username":"user1","result":"{\"status\":\"Accepted\"}","follow":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/follows/1787534744543","type":"Follow","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1"}}% ~ % 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,"alreadyFollowing":true,"target":"https://freelifemakers.com/users/user1","username":"user1","message":"์ด๋ฏธ ํ๋ก์ ์ค / Already following"}%
~ %
โ๏ธ ๊ธ ๋ฐฐ๋ฌ ํ์ธ / Confirming post delivery
https://aloy-horizon.duckdns.org/@user1

์ํ๋ณต์ 8์ฅ 32์ / John 8:32
“๊ทธ๋ฆฌ๊ณ ๋ํฌ๋ ์ง๋ฆฌ๋ฅผ ์๊ฒ ๋ ๊ฒ์ด๋ฉฐ, ์ง๋ฆฌ๊ฐ ๋ํฌ๋ฅผ ์์ ๋กญ๊ฒ ํ ๊ฒ์ด๋ค.”
“Then you will know the truth ,and the truth will set you free”