[nextjs]SNS Server-13(myapp23)

πŸ‘‰πŸ»μ’‹μ•„μš”μ™€ μ’‹μ•„μš” μ·¨μ†ŒκΈ°λŠ₯을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.
Implement the “like” and “unlike” functions.

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

myapp23/  
β”œβ”€β”€ 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
β”‚   β”œβ”€β”€ api/like/route.ts.     -> Like 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

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

npx create-next-app@latest

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

cd ~/myapp23
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

βœ”οΈ ν…Œμ΄λΈ” 및 인덱슀 생성(sqlite)
Create Table and Index (sqlite)

CREATE TABLE IF NOT EXISTS likes (
  id TEXT PRIMARY KEY,          
  actor TEXT NOT NULL,           
  object TEXT NOT NULL,          
  username TEXT NOT NULL,       
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_likes_object ON likes(object);
CREATE INDEX IF NOT EXISTS idx_likes_actor ON likes(actor);

1)id TEXT PRIMARY KEY,
— Like Activity ID: https://aloy-horizon.duckdns.org/users/user1/likes/1234

2)actor TEXT NOT NULL,
— μ’‹μ•„μš” λˆ„λ₯Έμ‚¬λžŒ / People who liked this

— https://freelifemakers.com/users/user1

3)object TEXT NOT NULL,
— μ’‹μ•„μš” λˆ„λ₯Έ κΈ€ / Posts I’ve liked

— https://aloy-horizon.duckdns.org/users/user1/statuses/abc

4) username TEXT NOT NULL,
— λ‚΄ 둜컬 μœ μ €/My local user:: user1

βœ”οΈ myapp23/lib/db.ts μ½”λ“œ μˆ˜μ •

import Database from 'better-sqlite3';
import path from 'path';

const dbPath = path.join(process.cwd(), 'data.sqlite');
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');

db.exec(`

... ...

  -- myapp23 βœ… likes/Undo likes
  CREATE TABLE IF NOT EXISTS likes (
    id TEXT PRIMARY KEY,           
    actor TEXT NOT NULL,    
    object TEXT NOT NULL,      
    username TEXT NOT NULL,       
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

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);
  CREATE UNIQUE INDEX IF NOT EXISTS idx_followers_actor_username ON followers(actor, username);
  -- myapp23 βœ…
  CREATE INDEX IF NOT EXISTS idx_likes_object ON likes(object);
  CREATE INDEX IF NOT EXISTS idx_likes_actor ON likes(actor);
`);

export default db;

πŸ“ μ½”λ“œ μˆ˜μ • 및 라우트 μΆ”κ°€
Code modifications and route additions

βœ”οΈ 라우트 μΆ”κ°€ / Add Route

— app/api/like/route.ts 

// app/api/like/route.ts
// myapp23 βœ… - Like / Undo Like

import { NextRequest, NextResponse } from 'next/server';
import db from '@/lib/db';
import { getActorData, sendLike, sendUndoLike, signedFetch } from '@/lib/ap';

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 });
    }

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

    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 in post');
        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) {
      console.error(e);
      return NextResponse.json({ ok: false, error: 'invalid target post id' }, { status: 400 });
    }

    // 쀑볡 μ²΄ν¬λŠ” apObjectId둜 ν•΄μ•Ό 함 (Mastodon λŒ€μ‘)
    const existing = db.prepare('SELECT id FROM likes WHERE object = ? AND username = ?').get(apObjectId, username) as any;
    if (existing) {
      return NextResponse.json({ ok: true, alreadyLiked: true, id: existing.id });
    }

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

    db.prepare('INSERT OR IGNORE INTO likes (id, actor, object, username) VALUES (?,?,?,?)')
      .run(result.likeDoc.id, result.likeDoc.actor, result.likeDoc.object, username);

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

  } catch (e: any) {
    console.error('[Like 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 likeRow = db.prepare('SELECT id FROM likes WHERE object = ? AND username = ?').get(apObjectId, username) as any;
    if (!likeRow) {
      return NextResponse.json({ ok: true, alreadyUnliked: true });
    }

    const result = await sendUndoLike(inbox, likeRow.id, apObjectId, username);

    db.prepare('DELETE FROM likes WHERE id = ?').run(likeRow.id);

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

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

βœ”οΈ μ½”λ“œμˆ˜μ • / Code modification

— lib/ap.ts에 sendLike, sendUndoLike μΆ”κ°€


... ...

// βœ… myapp23 - Like 보내기 / Send Like
export async function sendLike(toInbox: string, targetPostId: string, username: string) {
  const actorId = `https://${DOMAIN}/users/${username}`;
  const likeId = `${actorId}/likes/${Date.now()}`;

  const likeDoc = {
    '@context': 'https://www.w3.org/ns/activitystreams',
    id: likeId,
    type: 'Like',
    actor: actorId,
    object: targetPostId
  };

  const body = JSON.stringify(likeDoc);
  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}] Like 전솑 -> ${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(`πŸ“¬ Like κ²°κ³Ό: ${res.status}`, text);
  return { ok: res.ok, status: res.status, text, likeDoc };
}

// βœ… myapp23 - Undo Like 보내기 / Send Undo Like
export async function sendUndoLike(toInbox: string, likeId: string, targetPostId: string, username: string) {
  const actorId = `https://${DOMAIN}/users/${username}`;
  const undoId = `${actorId}#undo/${Date.now()}`;

  const undoDoc = {
    '@context': 'https://www.w3.org/ns/activitystreams',
    id: undoId,
    type: 'Undo',
    actor: actorId,
    object: {
      id: likeId,
      type: 'Like',
      actor: actorId,
      object: targetPostId
    }
  };

  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 Like 전솑 -> ${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 Like κ²°κ³Ό: ${res.status}`, text);
  return { ok: res.ok, status: res.status, text, undoDoc };
}

// βœ… myapp23 - Actor 데이터 κ°€μ Έμ˜€κΈ° (inbox/route, like/route 곡용)
export async function getActorData(actorUrl: string, username: string) {
  const res = await signedFetch(actorUrl, username);
  if (!res.ok) throw new Error(`getActorData failed: ${res.status} ${await res.text()}`);
  const data = await res.json();
  return {
    id: data.id,
    inbox: data.inbox
  };
}

— myapp23/app/users/[username]/inbox/route.ts에 Like 처리 μΆ”κ°€

export async function POST(req: Request, { params }: { params: Promise<{ username: string }> }) {
... ...
  if (body.type === 'Undo') {
       // Follow Undo인 경우 / If it's a Follow Undo
      if (body.object?.type === 'Follow' || typeof body.object === 'string' || body.object?.id?.includes('#follow')) {
         const unfollowActorId = body.actor; // λˆ„κ°€ μ–ΈνŒ”ν–ˆλŠ”μ§€ / who unfollowed
         //db.prepare('DELETE FROM followers WHERE actor = ?').run(unfollowActorId);

      // βœ… myapp15 - μ–ΈνŒ”λ‘œμš° μ‹œ username도 쑰건에 μΆ”κ°€ / Add username condition when unfollowing
         db.prepare('DELETE FROM followers WHERE actor = ? AND username = ?').run(unfollowActorId, username);
         console.log(`πŸ—‘οΈ [${username}] μ–ΈνŒ”λ‘œμš° / unfollow : ${unfollowActorId}`);
      }
      // βœ… myapp23 - Like Undo
      else if (objType === 'Like' || obj?.id?.includes('/likes/')) {
        const likeId = typeof obj === 'string'? obj : obj.id;
        db.prepare('DELETE FROM likes WHERE id =?').run(likeId);
        console.log(`πŸ’” [${username}] Unlike μ €μž₯ / unlike : ${likeId} by ${actorId}`);
      }

  }
}

... ...

    // βœ… myapp23 - μ’‹μ•„μš” λ°›κΈ° / Get Likes !
    if (body.type === 'Like') {
      try {
        const likeId = body.id;
        const objectId = typeof body.object === 'string'? body.object : body.object?.id;
        console.log(`❀️ [${username}] Like 도착: ${actorId} -> ${objectId}`);

        db.prepare('INSERT OR IGNORE INTO likes (id, actor, object, username) VALUES (?,?,?,?)')
         .run(likeId, actorId, objectId, username);

        console.log(`βœ… [${username}] likes μ €μž₯ μ™„λ£Œ: ${likeId}`);
      } catch (e) {
        console.error(`❌ Like μ €μž₯ μ‹€νŒ¨/
Failed to save 'Like'`, e);
      }
      return new Response('', { status: 202 });
    }

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

βœ”οΈ pinaforeμ—μ„œ μ’‹μ•„μš” λˆ„λ₯΄κΈ° / Like on Pinafore

pinafore

— λ‚΄ μ„œλ²„ 둜그 / My server log

 GET /users/user1/posts/c0ee1d4b-b09a-4062-b0e9-9b3b40f50bb3 404 in 328ms (next.js: 266ms, application-code: 62ms)
πŸ“© [user1] INBOX: Like https://freelifemakers.com/users/user1
⚠️ [user1] publicKey μ—†μŒ, 검증 μŠ€ν‚΅ / no publicKey, skip verify
❀️ [user1] Like 도착: https://freelifemakers.com/users/user1 -> https://aloy-horizon.duckdns.org/users/user1/posts/c0ee1d4b-b09a-4062-b0e9-9b3b40f50bb3
βœ… [user1] likes μ €μž₯ μ™„λ£Œ: https://freelifemakers.com/users/user1/liked/01M0XS4TFGAA6G2YPYE72P0YRA
 POST /users/user1/inbox 202 in 668ms (next.js: 547ms, application-code: 121ms)

— λ°μ΄ν„°λ² μ΄μŠ€ / Database(SQLITE)

1)likesν…Œμ΄λΈ” / likes table

myapp23 % sqlite3 data.sqlite
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.
sqlite> select * from likes;
https://freelifemakers.com/users/user1/liked/01M0XS4TFGAA6G2YPYE72P0YRA|https://freelifemakers.com/users/user1|https://aloy-horizon.duckdns.org/users/user1/posts/c0ee1d4b-b09a-4062-b0e9-9b3b40f50bb3|user1|2026-04-10 00:10:26
sqlite> 

βœ”οΈ freelifemakers.com의 글에 μ’‹μ•„μš” λˆ„λ₯΄κΈ° / Like posts on freelifemakers.com

— POST λ©”μ†Œλ“œλ‘œ νŠΉμ • ν¬μŠ€νŠΈμ— μ’‹μ•„μš” λˆ„λ₯΄κΈ°
Like a specific post using the POST method.

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/[POST_ID]"}'
myapp23 % 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"}'
{"ok":true,"result":{"ok":true,"status":202,"text":"{\"status\":\"Accepted\"}","likeDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/likes/1787708354555","type":"Like","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}}}%                                                                                                                                     
gimdaegyeong@gimdaegyeong-ui-MacBookAir myapp23 % 

1) κΈ°μ‘΄ κΈ€ 검색 / Search existing posts

myapp23 % sqlite3 data.sqlite
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.
sqlite> select * from inbox_posts;
https://freelifemakers.com/users/user1/statuses/01M0EJPP57WC47FAK0N395S7MB|https://freelifemakers.com/users/user1|<p>inbox_posts table test</p>|user1|2026-08-20 03:16:15|
https://freelifemakers.com/users/user1/statuses/01M0KMS8K1XRFW703S32MX6V25|https://freelifemakers.com/users/user1|<p>actor test</p>|user1|2026-08-22T11:28:48+09:00|https://freelifemakers.com/users/user1/statuses/01M0KMS8K1XRFW703S32MX6V25
01M0XT1VBF23F8Y5EXAPK1VY56|https://freelifemakers.com/users/user1|<p>likes test</p>|user1|2026-08-26T10:13:17+09:00|https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56
sqlite> 

2) μ’‹μ•„μš” μ‹€ν–‰ν•˜κΈ° / like posts on

A. ν„°λ―Έλ„μ—μ„œ μ‹€ν–‰ν•˜κΈ° / Running in the terminal

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"}'

B. 응닡메세지(μ„±κ³΅μΈκ²½μš°) / Response message (in case of success)

{"ok":true,"result":{"ok":true,"status":202,"text":"{\"status\":\"Accepted\"}","likeDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/likes/1787708354555","type":"Like","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}}}% 
Screenshot

C.λ‹€λ₯Έ λ§ˆμŠ€ν† λˆ(https://mastodon.social/users/Gargron/statuses/114559081070832514)의 κ²Œμ‹œλ¬Όμ— μ’‹μ•„μš” λˆ„λ₯΄κΈ°(성곡)
Liking a post from another Mastodon instance
(https://mastodon.social/users/Gargron/statuses/114559081070832514) (Success)

– λ‹€λ₯Έ λ§ˆμŠ€ν† λˆ κ²Œμ‹œλ¬Όμ— μ’‹μ•„μš” μ‹€ν–‰ν•˜κΈ° / Like other Mastodon posts

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

– 응닡성곡 / Response successful

{"ok":true,"result":{"ok":true,"status":202,"text":"","likeDoc":{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/likes/af39dead-cb1a-45ba-bf9d-f3074921c2a0","type":"Like","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/Gargron/statuses/114559081070832514"}}}%

3) μ’‹μ•„μš” μ·¨μ†Œ μ‹€ν–‰ / Perform Undo

A.gotosocial – freelifemakers.com

– μ’‹μ•„μš” μ·¨μ†Œ 전솑 / Undo Send

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

– 응닡성곡 / Response successful

  {"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/75038dda-f43b-4167-b49a-376ce2a0a29f","type":"Undo","actor":"https://aloy-horizon.duckdns.org/users/user1","object":{"id":"https://aloy-horizon.duckdns.org/users/user1/likes/09d8e157-6f0e-4320-af76-2bf337faf2f6","type":"Like","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}}}}

B.mastodon – https://mastodon.social/users/Gargron/statuses/114559081070832514

– μ’‹μ•„μš” μ·¨μ†Œ 전솑 / Undo Send

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

– 응닡성곡 / Response successful

 {"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/9aabf284-68a7-42a2-9761-1b7ee0b7bcb2","type":"Undo","actor":"https://aloy-horizon.duckdns.org/users/user1","object":{"id":"https://aloy-horizon.duckdns.org/users/user1/likes/af39dead-cb1a-45ba-bf9d-f3074921c2a0","type":"Like","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://mastodon.social/users/Gargron/statuses/114559081070832514"}}}}%

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

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

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

Leave a Reply