ππ» @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”