주제 투표 자동 자기 투표

:information_source: 요약 사용자가 토픽을 생성할 때 자동 토픽 투표를 활성화합니다
:hammer_and_wrench: 저장소 GitHub - dereklputnam/discourse-topic-voting-auto-self-vote · GitHub
:question: 설치 가이드 테마 또는 테마 컴포넌트 설치 방법
:open_book: 디스코URS 테마를 처음 사용하시나요? 디스코URS 테마 사용 입문 가이드

이 테마 컴포넌트를 설치합니다

여기 Meta와 같이 투표 횟수에 제한이 있는 커뮤니티의 경우, OP(최초 작성자)의 투표가 자동으로 실행되지 않는 것이 합리적입니다(관련 토론은 여기여기를 참조). 하지만 무제한 투표가 가능한 커뮤니티에서는 이것이 불편한 요소가 되며, 사용자가 자신의 토픽에 투표를 하지 않을 수도 있습니다. 따라서 이 컴포넌트가 필요합니다.

설정

  • 자동 투표 카테고리: 사이트 전체에 적용하지 않으려면 적용할 카테고리를 지정하세요.
  • 제외 그룹: 특정 그룹(예: 내부 팀)에 적용하지 않으려면 여기에 추가하세요.
  • 방문 시 자동 투표: OP가 투표하지 않은 기존 토픽을 정리하는 부드러운 방법입니다.

토픽 백필링(Backfilling)

대상 토픽 식별

OP가 투표하지 않은 토픽을 식별하기 위해 이 데이터 탐색기 쿼리를 사용하세요:

-- [params]
-- null category_id :category_id
-- null string :category_slug
-- boolean :exclude_about = true

SELECT
  t.id AS topic_id,
  u.username AS "username",
  t.title AS "Topic Title",
  t.user_id AS "Author",
  t.created_at AS "Created At",
  c.name AS "Category Name",
  c.slug AS "Category Slug"
FROM topics t
JOIN users u ON u.id = t.user_id
JOIN categories c ON c.id = t.category_id
LEFT JOIN topic_voting_topic_vote_count tvvc ON tvvc.topic_id = t.id
LEFT JOIN topic_voting_votes tvv ON tvv.topic_id = t.id AND tvv.user_id = t.user_id
WHERE t.deleted_at IS NULL
  AND (
    tvvc.votes_count IS NULL OR tvv.id IS NULL
  )
  AND (
    :exclude_about = false
    OR t.title ILIKE 'about the % category' = false
  )
  AND (
    :category_id IS NULL OR c.id = :category_id
  )
  AND (
    :category_slug IS NULL OR c.slug = :category_slug
  )

팁: 토픽 업데이트 후 이 쿼리를 다시 실행하여 투표가 실행되었는지 확인하세요 :ballot_box:

API를 통한 백필링

그 다음, 해당 목록을 사용하여 API 스크립트를 실행하여 투표를 백필링하세요! 아래의 제 스크립트를 사용하려면 첫 번째 두 열을 제외한 나머지 열을 제거해야 합니다. 커뮤니티 내에서 토픽을 식별하기 쉽게 하기 위해 데이터 탐색기 쿼리에 남겨두었습니다.

참고: 각 사용자를 대신하여 행동하므로 API 키는 모든 사용자에 대한 범위로 설정되어야 합니다.

사용한 스크립트를 첨부할 권한이 없지만, 텍스트로 공유합니다:

백필 스크립트
#!/usr/bin/env python3
"""
Discourse API를 통한 셀프 투표 백필링

이 스크립트는 자신의 토픽에 투표하지 않은 토픽 작성자를 대신하여 투표를 실행합니다.
사용자를 대신하여 투표를 실행하기 위해 Discourse API를 사용합니다.

요구 사항:
- Python 3.7+
- requests 라이브러리 (pip install requests)
- 사용자 대역(impersonation) 권한이 있는 관리자 API 키

사용 방법:
1. 아래 구성 섹션을 업데이트하세요
2. topic_id와 username 열이 있는 CSV 파일을 준비하세요
3. 실행: python backfill_votes_api.py

CSV 형식:
    topic_id,username
    12345,john_doe
    12346,jane_smith
"""

import csv
import time
import requests
from datetime import datetime

#==============================================================================
# CONFIGURATION
#==============================================================================

# Discourse 인스턴스 URL (끝에 슬래시 없음)
DISCOURSE_URL = 'https://yourcommunity.com'

# 관리자 API 키 (사용자 대역 권한이 있어야 함)
API_KEY = 'YOUR_API_KEY_HERE'

# API 키를 소유한 관리자 사용자 이름
API_USERNAME = 'system'

# topic_id와 username 열이 있는 CSV 파일 경로
CSV_FILE = 'topics_to_vote.csv'

# DRY RUN 모드 - 실제 투표를 실행하려면 False로 설정
DRY_RUN = True

# 레이트 리미팅을 피하기 위한 API 호출 간 지연 시간 (초)
DELAY_BETWEEN_REQUESTS = 0.5

#==============================================================================
# SCRIPT
#==============================================================================

def cast_vote(topic_id: int, username: str) -> dict:
    """
    특정 사용자로 하여금 토픽에 투표하게 합니다.

    Args:
        topic_id: 투표할 토픽의 ID
        username: 투표 시 대역할 사용자 이름

    Returns:
        'success' 부울 값과 'message' 문자열이 포함된 dict
    """
    url = f"{DISCOURSE_URL}/voting/vote"

    headers = {
        'Api-Key': API_KEY,
        'Api-Username': username,  # 사용자 대역
        'Content-Type': 'application/json'
    }

    data = {
        'topic_id': topic_id
    }

    try:
        response = requests.post(url, headers=headers, json=data)

        if response.status_code == 200:
            return {'success': True, 'message': 'Vote cast successfully'}
        elif response.status_code == 422:
            # 일반적으로 이미 투표했거나 투표가 비활성화된 경우
            return {'success': False, 'message': 'Already voted or voting not enabled'}
        elif response.status_code == 403:
            return {'success': False, 'message': 'Permission denied - check API key permissions'}
        elif response.status_code == 404:
            return {'success': False, 'message': 'Topic not found or voting not enabled on category'}
        else:
            return {'success': False, 'message': f'HTTP {response.status_code}: {response.text[:200]}'}

    except requests.RequestException as e:
        return {'success': False, 'message': f'Request error: {str(e)}'}


def main():
    print("\n" + "=" * 60)
    print("Backfill Self-Votes via API")
    print("=" * 60)
    print(f"Mode: {'DRY RUN (no changes)' if DRY_RUN else 'LIVE (votes will be cast)'}")
    print(f"Target: {DISCOURSE_URL}")
    print(f"CSV File: {CSV_FILE}")
    print("=" * 60 + "\n")

    # CSV 파일 읽기
    try:
        with open(CSV_FILE, 'r', newline='', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            rows = list(reader)
    except FileNotFoundError:
        print(f"ERROR: CSV file not found: {CSV_FILE}")
        print("\nCreate a CSV file with the following format:")
        print("topic_id,username")
        print("12345,john_doe")
        print("12346,jane_smith")
        return
    except Exception as e:
        print(f"ERROR: Failed to read CSV file: {e}")
        return

    if not rows:
        print("ERROR: CSV file is empty")
        return

    # CSV 열 검증
    columns = set(rows[0].keys())

    if 'topic_id' not in columns or 'username' not in columns:
        print(f"ERROR: CSV must have columns: topic_id and username")
        print(f"Found columns: {columns}")
        return

    print(f"Found {len(rows)} topics to process\n")

    # 각 행 처리
    success_count = 0
    skip_count = 0
    error_count = 0

    for i, row in enumerate(rows, 1):
        topic_id = row['topic_id'].strip()
        username = row['username'].strip()

        print(f"[{i}/{len(rows)}] Topic #{topic_id} by @{username}", end=" ")

        if DRY_RUN:
            print("-> would vote")
            success_count += 1
        else:
            result = cast_vote(int(topic_id), username)

            if result['success']:
                print("-> voted!")
                success_count += 1
            elif 'Already voted' in result['message']:
                print("-> already voted (skipped)")
                skip_count += 1
            else:
                print(f"-> ERROR: {result['message']}")
                error_count += 1

            # 레이트 리미팅
            if i < len(rows):
                time.sleep(DELAY_BETWEEN_REQUESTS)

    # 요약
    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    print(f"Total topics: {len(rows)}")
    print(f"Votes {'to cast' if DRY_RUN else 'cast'}: {success_count}")
    print(f"Already voted (skipped): {skip_count}")
    print(f"Errors: {error_count}")

    if DRY_RUN:
        print("\n** DRY RUN COMPLETE **")
        print("To cast votes, set DRY_RUN = False and run again.")

    print("")


if __name__ == '__main__':
    main()
4개의 좋아요