[nextjs]SNS Server-14(myapp24)

πŸ‘‰πŸ» myapp24μ—μ„œλŠ” Announce (Boost) λ₯Ό κ΅¬ν˜„ν•©λ‹ˆλ‹€.
myapp24 implements Announce (Boost).

πŸ‘‰πŸ» λΆ€μŠ€νŠΈλŠ” λ‹€λ₯Έ μ‚¬λžŒμ΄ μ“΄ 글을 λ‚΄ νŒ”λ‘œμ›Œμ—κ²Œ μ•Œλ¦¬λŠ” κΈ°λŠ₯μž…λ‹ˆλ‹€.
Boost is a feature that shares posts written by others with your followers.

πŸ‘‰πŸ» λ‚΄κ°€ μ“΄ 글은 λΆ€μŠ€νŠΈλ₯Ό ν•  수 μ—†μŠ΅λ‹ˆλ‹€.
I cannot boost the posts I have written.

πŸ“ 전체 ν”„λ‘œμ νŠΈ ꡬ쑰 / Overall Project Structure

myapp24/  
β”œβ”€β”€ app/  (Next.js App Router)
β”‚   β”œβ”€β”€ .well-known/webfinger/route.ts  -> webfinger
β”‚   β”œβ”€β”€ api/follow/route.ts    -> Follow API(temporary)
β”‚   β”œβ”€β”€ api/announce/route.ts  -> Boost(Announcement)
β”‚   β”œβ”€β”€ api/like/route.ts.     -> Like API
β”‚   β”œβ”€β”€ api/posts/route.ts     -> Writing API
β”‚   β”œβ”€β”€ 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

πŸ“ ν”„λ‘œμ νŠΈ μ‹œμž‘(myapp24)
Project Start (myapp24)

npx create-next-app@latest

πŸ“ SQLiteμ„€μΉ˜ / Installing SQLite

cd ~/myapp24
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;

πŸ“ ν…Œμ΄λΈ” μΆ”κ°€ / Add Table

CREATE TABLE IF NOT EXISTS announces (
  id TEXT PRIMARY KEY, 
  username TEXT, 
  object TEXT, 
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_announces_obj_user ON announces(object, username);
CREATE INDEX IF NOT EXISTS idx_announces_username ON announces(username);

πŸ“ λΌμš°νŠΈμΆ”κ°€ / Add route

βœ”οΈ myapp24/app/api/announce/route.ts μƒμ„±

// myapp24/app/api/announce/route.ts
// myapp24 βœ…

import { NextRequest, NextResponse } from 'next/server';
import db from '@/lib/db';
import { signedFetch, getActorData, sendAnnounce, sendUndoAnnounce } from '@/lib/ap';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org'; 

export async function POST(req: NextRequest) {
  try {
    const { username, target } = await req.json();
    if (!username || !target) {
      return NextResponse.json({ ok: false, error: 'username and target required' }, { status: 400 });
    }

    if (target.startsWith(`https://${DOMAIN}/users/${username}`)) {
      return NextResponse.json({ ok: false, error: 'cannot boost own post' }, { status: 400 });
    }

    let apObjectId: string = target;
    let inbox: string;

    try {
      const postRes = await signedFetch(target, username);
      if (postRes.ok) {
        const postData = await postRes.json();
        apObjectId = postData.id || target;
        const attributedTo = postData.attributedTo || postData.actor;
        const actorUrl = typeof attributedTo === 'string' ? attributedTo : attributedTo?.id;
        if (!actorUrl) throw new Error('no actor');
        const actorInfo = await getActorData(actorUrl, username);
        inbox = actorInfo.inbox;
      } else {
        const url = new URL(target);
        const parts = url.pathname.split('/');
        const usersIdx = parts.indexOf('users');
        if (usersIdx === -1) throw new Error('invalid target');
        const actorUrl = `${url.origin}${parts.slice(0, usersIdx + 2).join('/')}`;
        const actorInfo = await getActorData(actorUrl, username);
        inbox = actorInfo.inbox;
      }
    } catch (e) {
      return NextResponse.json({ ok: false, error: 'invalid target post id' }, { status: 400 });
    }

    const existing = db.prepare('SELECT id FROM announces WHERE object = ? AND username = ?').get(apObjectId, username) as any;
    if (existing) {
      return NextResponse.json({ ok: true, alreadyAnnounced: true, id: existing.id });
    }

    const result = await sendAnnounce(inbox, apObjectId, username);

    if (result.ok) {
      db.prepare('INSERT INTO announces (id, username, object) VALUES (?, ?, ?)').run(result.announceDoc.id, username, apObjectId);
    }

    return NextResponse.json({ ok: true, result });

  } catch (e: any) {
    console.error('[Announce POST]', e);
    return NextResponse.json({ ok: false, error: e.message }, { status: 500 });
  }
}

export async function DELETE(req: NextRequest) {
  try {
    const { username, target } = await req.json();
    if (!username || !target) {
      return NextResponse.json({ ok: false, error: 'username and target required' }, { status: 400 });
    }

    let apObjectId: string = target;
    let inbox: string;

    try {
      const postRes = await signedFetch(target, username);
      if (postRes.ok) {
        const postData = await postRes.json();
        apObjectId = postData.id || target;
        const attributedTo = postData.attributedTo || postData.actor;
        const actorUrl = typeof attributedTo === 'string' ? attributedTo : attributedTo?.id;
        if (!actorUrl) throw new Error('no actor');
        const actorInfo = await getActorData(actorUrl, username);
        inbox = actorInfo.inbox;
      } else {
        const url = new URL(target);
        const parts = url.pathname.split('/');
        const usersIdx = parts.indexOf('users');
        if (usersIdx === -1) throw new Error('invalid target');
        const actorUrl = `${url.origin}${parts.slice(0, usersIdx + 2).join('/')}`;
        const actorInfo = await getActorData(actorUrl, username);
        inbox = actorInfo.inbox;
      }
    } catch (e) {
      return NextResponse.json({ ok: false, error: 'invalid target post id' }, { status: 400 });
    }

    const row = db.prepare('SELECT id FROM announces WHERE object = ? AND username = ?').get(apObjectId, username) as any;
    if (!row) {
      return NextResponse.json({ ok: true, alreadyUnAnnounced: true });
    }

    const result = await sendUndoAnnounce(inbox, row.id, apObjectId, username);
    db.prepare('DELETE FROM announces WHERE id = ?').run(row.id);

    return NextResponse.json({ ok: true, result });

  } catch (e: any) {
    console.error('[Announce DELETE]', e);
    return NextResponse.json({ ok: false, error: e.message }, { status: 500 });
  }
}

πŸ“ μ½”λ“œ μˆ˜μ • / Code Modification

βœ”οΈmyapp24/lib/db.ts

db.exec(`
  -- myapp24 βœ…  Boost(Announcement)
  CREATE TABLE IF NOT EXISTS announces (
    id TEXT PRIMARY KEY, 
    username TEXT, 
    object TEXT, 
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);
db.exec(`
... ...
  -- myapp24 βœ…
  CREATE INDEX IF NOT EXISTS idx_announces_obj_user ON announces(object, username);
  CREATE INDEX IF NOT EXISTS idx_announces_username ON announces(username);
`);

βœ”οΈmyapp24/lib/ap.ts

... ...

// getActorDataμ•„λž˜μ— μ½”λ“œ μΆ”κ°€ν•˜κΈ°
// Add code below getActorData

// βœ… myapp24 - Send Announce(Boost) 
export async function sendAnnounce(toInbox: string, targetPostId: string, username: string) {
  const actorId = `https://${DOMAIN}/users/${username}`;
  const announceId = `${actorId}/announces/${crypto.randomUUID()}`;

  const doc = {
    '@context': 'https://www.w3.org/ns/activitystreams',
    id: announceId,
    type: 'Announce',
    actor: actorId,
    object: targetPostId,
    published: new Date().toISOString(),
    to: ['https://www.w3.org/ns/activitystreams#Public'],
    cc: [`${actorId}/followers`, `${actorId}`],
  };

  const body = JSON.stringify(doc);
  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}] Announce 전솑 -> ${toInbox} (${targetPostId})`);

  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(`πŸ“¬ Announce κ²°κ³Ό: ${res.status}`, text);
  return { ok: res.ok, status: res.status, text, announceDoc: doc };
}

// βœ… myapp24 - Undo Announce
export async function sendUndoAnnounce(toInbox: string, announceId: string, targetPostId: string, username: string) {
  const actorId = `https://${DOMAIN}/users/${username}`;
  const undoId = `${actorId}#undo/${crypto.randomUUID()}`;

  const undoDoc = {
    '@context': 'https://www.w3.org/ns/activitystreams',
    id: undoId,
    type: 'Undo',
    actor: actorId,
    published: new Date().toISOString(),
    to: ['https://www.w3.org/ns/activitystreams#Public'],
    cc: [`${actorId}/followers`],
    object: {
      id: announceId,
      type: 'Announce',
      actor: actorId,
      object: targetPostId,
      published: new Date().toISOString(),
      to: ['https://www.w3.org/ns/activitystreams#Public'],
      cc: [`${actorId}/followers`],
    }
  };

  const body = JSON.stringify(undoDoc);
  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}] Undo Announce 전솑 -> ${toInbox}`);

  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(`πŸ“¬ Undo Announce κ²°κ³Ό: ${res.status}`, text);
  return { ok: res.ok, status: res.status, text, undoDoc };
}

πŸ“ ν…ŒμŠ€νŠΈ / Test

βœ”οΈ POST,DELETE λ©”μ†Œλ“œ 라우트
POST and DELETE method routes

# -- freelifemakers.com(gotosocial) --
# λΆ€μŠ€νŠΈ / Boost

curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/@user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG"}'


# λΆ€μŠ€νŠΈ μ·¨μ†Œ / Cancle Boost

curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/@user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG"}'

# -- mastodon server ---
# λΆ€μŠ€νŠΈ / Boost

curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089028658055432"}'

# λΆ€μŠ€νŠΈ μ·¨μ†Œ / Cancle Boost

curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089028658055432"}'

# λ‚΄ κΈ€ λΆ€μŠ€νŠΈ μ‹œλ„ -> 400 μ—λŸ¬ λ‚˜μ•Ό 정상
# Attempting to boost my post -> A 400 error is the expected result.

curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://aloy-horizon.duckdns.org/users/user1/statuses/e6c99652-572f-491a-9e61-05e67b0d58fc"}'

βœ”οΈ GTS Server

# GTS Boost

myapp24 % curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/@user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG"}'

{"ok":true,"result":{"ok":true,"status":202,"text":"{\"status\":\"Accepted\"}","announceDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/announces/66cb2e85-0bd0-4e6c-856a-d77070cc3807","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG","published":"2026-08-27T00:50:15.162Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers","https://aloy-horizon.duckdns.org/users/user1"]}}}%                                                              
gimdaegyeong@gimdaegyeong-ui-MacBookAir myapp24 %   

# GTS Boost Cancle  

myapp24 % curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/@user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG"}'

{"ok":true,"result":{"ok":true,"status":202,"text":"{\"status\":\"Accepted\"}","undoDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1#undo/34cb45a0-c1f1-4bcb-bb64-2cb9a0aba982","type":"Undo","actor":"https://aloy-horizon.duckdns.org/users/user1","published":"2026-08-27T00:53:42.406Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers"],"object":{"id":"https://aloy-horizon.duckdns.org/users/user1/announces/66cb2e85-0bd0-4e6c-856a-d77070cc3807","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG","published":"2026-08-27T00:53:42.406Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers"]}}}}%   
GTS Boost

βœ”οΈ Mastodon Server

— κ²Œμ‹œλ¬Ό μ£Όμ†Œ / Post address

https://mastodon.social/@mcnees/117089028658055432

# Boost

myapp24 % curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089028658055432"}'

{"ok":true,"result":{"ok":true,"status":202,"text":"","announceDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/announces/827c782f-9422-43d4-bc14-0fed1cf648b6","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/mcnees/statuses/117089028658055432","published":"2026-08-27T00:58:19.780Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers","https://aloy-horizon.duckdns.org/users/user1"]}}}%                                                                                                 
gimdaegyeong@gimdaegyeong-ui-MacBookAir myapp24 % 

# Cancle Boost

myapp24 % curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@mcnees/117089028658055432"}'
{"ok":true,"result":{"ok":true,"status":202,"text":"","undoDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1#undo/3c643e41-dcb6-4e5c-ae6c-21c0ab5820b3","type":"Undo","actor":"https://aloy-horizon.duckdns.org/users/user1","published":"2026-08-27T00:59:54.564Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers"],"object":{"id":"https://aloy-horizon.duckdns.org/users/user1/announces/827c782f-9422-43d4-bc14-0fed1cf648b6","type":"Announce","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/mcnees/statuses/117089028658055432","published":"2026-08-27T00:59:54.564Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers"]}}}}%                                                                                                                                                         
Mastodon Boost

βœ”οΈ λ‚΄ κΈ€ λΆ€μŠ€νŠΈ 차단확인 / Check if my post is blocked from the Boost feature

myapp24 % curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://aloy-horizon.duckdns.org/users/user1/statuses/e6c99652-572f-491a-9e61-05e67b0d58fc"}'

{"ok":false,"error":"cannot boost own post"}%                                                                                                                           

πŸ“ μ§€κΈˆκΉŒμ§€ μž‘μ—…ν•œ λ‚΄μš©κ³Ό 라우트 μž…λ‹ˆλ‹€.
Here is the work done so far and the routes.

βœ”οΈ λΈŒλΌμš°μ €μ—μ„œ ν…ŒμŠ€νŠΈ ν•  수 μžˆλŠ” 라우트
Routes that can be tested in a browser

# 1. WebFinger
https://aloy-horizon.duckdns.org/.well-known/webfinger?resource=acct:user1@aloy-horizon.duckdns.org

# 2. Actor (ν”„λ‘œν•„ / Profile)
https://aloy-horizon.duckdns.org/users/user1

# 3. Followers (남이 λ‚˜ νŒ”λ‘œμš° / Someone follows me)
https://aloy-horizon.duckdns.org/users/user1/followers

# 4. Following (λ‚΄κ°€ 남을 νŒ”λ‘œμš° / I follow others)
https://aloy-horizon.duckdns.org/users/user1/following

# 5. Outbox (λ‚΄ κΈ€ λͺ©λ‘ / My post list)
https://aloy-horizon.duckdns.org/users/user1/outbox

# 6. νƒ€μž„λΌμΈ / Timeline

# - νƒ€μž„λΌμΈUI / Timeline UI
https://aloy-horizon.duckdns.org/usersui/user1
# or
https://aloy-horizon.duckdns.org/@user1

# - νƒ€μž„λΌμΈAPI / Timeline API
https://aloy-horizon.duckdns.org/api/timeline?username=user1

# 7. κ°œλ³„ κΈ€(κΈ€ 상세) / indivisual posts(Post Details)[posts, inbox_posts]
https://aloy-horizon.duckdns.org/users/user1/statuses/[ID]

# ex)
# posts table
https://aloy-horizon.duckdns.org/users/user1/statuses/e6c99652-572f-491a-9e61-05e67b0d58fc

# inbox_posts table
https://aloy-horizon.duckdns.org/users/user1/statuses/01M0P8VYEQTFW28N97W6MG8PY5

βœ”οΈ ν„°λ―Έλ„μ—μ„œλ§Œ ν…ŒμŠ€νŠΈ κ°€λŠ₯ν•œ 라우트
Routes that can only be tested 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 Fediverse! #test"}'

# μ’‹μ•„μš” μ‹€ν–‰ / Likes
curl -X POST https://aloy-horizon.duckdns.org/api/like \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}'

# μ’‹μ•„μš” μ·¨μ†Œν•˜κΈ° / Undo Likes
curl -X DELETE https://aloy-horizon.duckdns.org/api/like \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://mastodon.social/@Gargron/114559081070832514"}'

# λΆ€μŠ€νŠΈ μ‹€ν–‰ / Boost
curl -X POST https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/@user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG"}'

# λΆ€μŠ€νŠΈ μ·¨μ†Œ / Cancle Boost
curl -X DELETE https://aloy-horizon.duckdns.org/api/announce \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/@user1/statuses/01M10AHFHCKGF9BY5G4H50VSHG"}'

μš”ν•œλ³΅μŒ 8μž₯ 32절 / John 8:32

“그리고 λ„ˆν¬λŠ” 진리λ₯Ό μ•Œκ²Œ 될 것이며, 진리가 λ„ˆν¬λ₯Ό 자유둭게 ν•  것이닀.”

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

Leave a Reply