FastComments.com

FastComments를 사용하면 댓글이 시스템에 추가되거나, 업데이트되거나, 제거될 때마다 API 엔드포인트를 호출할 수 있습니다.

우리는 이를 HTTP/HTTPS를 통한 비동기 웹후크로 구현합니다.

웹후크란 무엇인가 Internal Link


웹훅은 두 시스템 간의 메커니즘 또는 통합으로, "생산자" (FastComments)가 이벤트를 발생시키고 "소비자" (사용자)가 API 호출을 통해 해당 이벤트를 소비합니다.


지원되는 이벤트 및 리소스 Internal Link


FastComments는 Comment 리소스에 대해서만 웹훅을 지원합니다.

댓글 생성, 삭제 및 업데이트에 대한 웹훅을 지원합니다.

이들 각각은 시스템에서 별개의 이벤트로 간주되며, 따라서 웹훅 이벤트에 대해 서로 다른 의미 와 구조를 가집니다.


테스트 Internal Link

Webhooks admin에는 각 이벤트 유형(Create, Update, Delete)마다 Send Test Payload 버튼이 있습니다. Create 및 Update 이벤트는 더미 WebhookComment 객체를 전송하고, Delete 테스트는 ID만 포함된 더미 요청 본문을 전송합니다.

페이로드 검증

웹훅 통합을 테스트할 때 들어오는 요청에 다음 헤더들이 포함되어 있는지 확인하세요:

  1. token - 귀하의 API 비밀
  2. X-FastComments-Timestamp - Unix 타임스탬프(초)
  3. X-FastComments-Signature - HMAC-SHA256 서명

HMAC 서명 검증을 사용해 페이로드가 진본인지 확인하세요.

테스트 도구

개발 중에 들어오는 웹훅 페이로드를 검사하려면 webhook.site 또는 ngrok 같은 도구를 사용할 수 있습니다.

이벤트 유형

  • Create Event: 새 댓글이 생성될 때 트리거됩니다. 기본 메서드: PUT
  • Update Event: 댓글이 수정될 때 트리거됩니다. 기본 메서드: PUT
  • Delete Event: 댓글이 삭제될 때 트리거됩니다. 기본 메서드: DELETE

각 이벤트는 요청 본문에 전체 댓글 데이터를 포함합니다 (페이로드 형식은 데이터 구조를 참조하세요).


데이터 구조 Internal Link

웹훅을 통해 전송되는 유일한 구조체는 아래 TypeScript에 설명된 WebhookComment 객체입니다.

WebhookComment 객체 구조

"Create" 이벤트 구조

"create" 이벤트의 요청 본문은 WebhookComment 객체입니다.

"Update" 이벤트 구조

"update" 이벤트의 요청 본문은 WebhookComment 객체입니다.

"Delete" 이벤트 구조

"delete" 이벤트의 요청 본문은 WebhookComment 객체입니다.

20231114일 기준 변경사항
이전에는 "delete" 이벤트의 요청 본문에 댓글 id만 포함되어 있었습니다. 이제는 삭제 시점의 전체 댓글이 포함됩니다.
WebhookComment 객체
Copy CopyRun External Link
1
2interface WebhookComment {
3 /** 댓글의 id. **/
4 id: string
5 /** 댓글 스레드를 식별하는 id 또는 URL. 정규화됨. **/
6 urlId: string
7 /** 댓글이 작성된 위치를 가리키는 URL. **/
8 url?: string
9 /** 댓글을 남긴 사용자의 id. SSO의 경우 tenant id가 접두사로 붙습니다. **/
10 userId?: string
11 /** 댓글을 남긴 사용자의 이메일. **/
12 commenterEmail?: string
13 /** 댓글 위젯에 표시되는 사용자 이름. SSO에서는 displayName일 수 있습니다. **/
14 commenterName: string
15 /** 원본 댓글 텍스트. **/
16 comment: string
17 /** 파싱된 이후의 댓글 텍스트. **/
18 commentHTML: string
19 /** 댓글 외부 id. **/
20 externalId?: string
21 /** 상위(부모) 댓글의 id. **/
22 parentId?: string | null
23 /** 댓글이 남겨진 UTC 날짜. **/
24 date: UTC_ISO_DateString
25 /** 투표의 결합된 카르마 (up - down). **/
26 votes: number
27 votesUp: number
28 votesDown: number
29 /** 사용자가 댓글을 남길 당시 로그인했거나, 댓글이 인증되었거나, 또는 세션을 인증한 경우 true. **/
30 verified: boolean
31 /** 댓글이 인증된 날짜. **/
32 verifiedDate?: number
33 /** 모더레이터가 댓글을 검토됨으로 표시했는지 여부. **/
34 reviewed: boolean
35 /** 아바타의 위치 또는 base64 인코딩. SSO로 전달된 값이 base64인 경우에만 base64가 됩니다. **/
36 avatarSrc?: string
37 /** 댓글이 수동 또는 자동으로 스팸으로 표시되었는지 여부. **/
38 isSpam: boolean
39 /** 댓글이 자동으로 스팸으로 분류되었는지 여부. **/
40 aiDeterminedSpam: boolean
41 /** 댓글에 이미지가 포함되어 있는지 여부. **/
42 hasImages: boolean
43 /** "Most Relevant" 정렬 기준에서 댓글이 위치한 페이지 번호. **/
44 pageNumber: number
45 /** "Oldest First" 정렬 기준에서 댓글이 위치한 페이지 번호. **/
46 pageNumberOF: number
47 /** "Newest First" 정렬 기준에서 댓글이 위치한 페이지 번호. **/
48 pageNumberNF: number
49 /** 댓글이 자동으로 또는 수동으로 승인되었는지 여부. **/
50 approved: boolean
51 /** 댓글 작성 시 사용자의 로케일 코드(형식: en_us). **/
52 locale: string
53 /** 댓글에 작성되어 성공적으로 파싱된 @멘션들. **/
54 mentions?: CommentUserMention[]
55 /** 댓글이 속한 도메인. **/
56 domain?: string
57 /** 이 댓글과 연결된 선택적 모더레이션 그룹 id 목록. **/
58 moderationGroupIds?: string[]|null
59}
60

When users are tagged in a comment, the information is stored in a list called mentions. Each object in that list has the following structure.

Webhook 멘션 객체
Copy CopyRun External Link
1
2interface CommentUserMention {
3 /** 사용자 id. SSO 사용자일 경우 tenant id가 접두사로 붙습니다. **/
4 id: string
5 /** @ 심볼을 포함한 최종 @mention 태그 텍스트. **/
6 tag: string
7 /** @ 심볼을 포함한 원본 @mention 태그 텍스트. **/
8 rawTag: string
9 /** 태그된 사용자 유형. user = FastComments.com 계정, sso = SSOUser. **/
10 type: 'user'|'sso'
11 /** 사용자가 알림 수신을 거부했더라도, 이 값은 여전히 true로 설정됩니다. **/
12 sent: boolean
13}
14

HTTP 메서드

관리자 패널에서 각 웹훅 이벤트 유형에 대한 HTTP 메서드를 구성할 수 있습니다:

  • Create 이벤트: POST 또는 PUT (기본값: PUT)
  • Update 이벤트: POST 또는 PUT (기본값: PUT)
  • Delete 이벤트: DELETE, POST 또는 PUT (기본값: DELETE)

모든 요청에 ID가 포함되어 있으므로 Create 및 Update 작업은 기본적으로 멱등성(PUT)을 가집니다. 동일한 Create 또는 Update 요청을 반복해도 귀측에서 중복 객체가 생성되지 않아야 합니다.

요청 헤더

각 웹훅 요청에는 다음 헤더들이 포함됩니다:

Header설명
Content-Typeapplication/json
token귀하의 API 시크릿
X-FastComments-Timestamp요청이 서명된 시점의 Unix 타임스탬프(초)
X-FastComments-SignatureHMAC-SHA256 서명 (sha256=<hex>)

See Security & API Tokens for information on verifying the HMAC signature.

보안 및 API 토큰 Internal Link


FastComments 웹훅 요청에는 보안을 위해 여러 인증 메커니즘이 포함되어 있습니다.

전송되는 헤더

헤더설명
token귀하의 API Secret(하위 호환성용)
X-FastComments-Timestamp요청이 서명될 때의 Unix 타임스탬프(초)
X-FastComments-Signature페이로드의 HMAC-SHA256 서명

HMAC 서명 검증 (권장)

웹훅 페이로드가 진본이며 변조되지 않았는지 확인하기 위해 HMAC 서명 검증을 강력히 권장합니다.

서명 형식: sha256=<hex-encoded-signature>

서명이 계산되는 방법:

  1. 결합: timestamp + "." + JSON_payload_body
  2. API Secret을 키로 사용하여 HMAC-SHA256을 계산
  3. 결과를 16진수로 인코딩

예시 검증 (Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(req, apiSecret) {
    const timestamp = req.headers['x-fastcomments-timestamp'];
    const signature = req.headers['x-fastcomments-signature'];

    if (!timestamp || !signature) {
        return false;
    }

    // 타임스탬프가 최근인지 확인합니다 (5분 이내)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
        return false;  // 재생 공격 방지
    }

    // 서명 검증
    const payload = JSON.stringify(req.body);
    const expectedSignature = crypto
        .createHmac('sha256', apiSecret)
        .update(`${timestamp}.${payload}`)
        .digest('hex');

    return signature === `sha256=${expectedSignature}`;
}

예시 검증 (Python)

import hmac
import hashlib
import time
import json

def verify_webhook_signature(headers, body, api_secret):
    timestamp = headers.get('X-FastComments-Timestamp')
    signature = headers.get('X-FastComments-Signature')

    if not timestamp or not signature:
        return False

    # 타임스탬프가 최신인지 확인합니다
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False

    # 서명 검증
    payload = json.dumps(body, separators=(',', ':'))
    message = f"{timestamp}.{payload}"
    expected = hmac.new(
        api_secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    return signature == f"sha256={expected}"

예시 검증 (PHP)

function verifyWebhookSignature($headers, $body, $apiSecret) {
    $timestamp = $headers['X-FastComments-Timestamp'] ?? null;
    $signature = $headers['X-FastComments-Signature'] ?? null;

    if (!$timestamp || !$signature) {
        return false;
    }

    // 타임스탬프가 최신인지 확인합니다 (5분 이내)
    $now = time();
    if (abs($now - intval($timestamp)) > 300) {
        return false;
    }

    // 서명 검증
    $payload = json_encode($body, JSON_UNESCAPED_SLASHES);
    $message = $timestamp . '.' . $payload;
    $expectedSignature = 'sha256=' . hash_hmac('sha256', $message, $apiSecret);

    return hash_equals($expectedSignature, $signature);
}

레거시 인증

하위 호환성을 위해 귀하의 API Secret이 포함된 token 헤더는 여전히 전송됩니다. 그러나 HMAC 검증은 재생 공격으로부터 보호하므로 보안 향상을 위해 HMAC 검증으로 마이그레이션할 것을 권장합니다.



결론

이로써 우리의 Webhooks 문서가 마무리됩니다.

FastComments Webhook 통합이 이해하기 쉽고 빠르게 설정되기를 바랍니다.

문서에서 누락된 부분을 발견하셨다고 생각되면 아래에 알려주세요.