Голосование за темы: автоматическое голосование за себя

:information_source: Краткое описание Включение автоматических голосов за темы при их создании пользователем
:hammer_and_wrench: Репозиторий GitHub - dereklputnam/discourse-topic-voting-auto-self-vote · GitHub
:question: Руководство по установке Как установить тему или компонент темы
:open_book: Новичок в темах Discourse? Начинающее руководство по использованию тем Discourse

Установить этот компонент темы

Если в вашем сообществе количество голосов ограничено, как, например, здесь на Meta, логично, чтобы голос автора темы (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
"""
Заполнение самоголосования через API Discourse

Этот скрипт выставляет голоса за авторов тем, которые не голосовали за свои собственные темы.
Он использует API Discourse для имитации пользователей и выставления голосов от их имени.

Требования:
- Python 3.7+
- библиотека requests (pip install requests)
- Административный API-ключ с правами на имитацию

Использование:
1. Обновите секцию конфигурации ниже
2. Подготовьте CSV-файл со столбцами topic_id и username
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

#==============================================================================
# КОНФИГУРАЦИЯ
#==============================================================================

# URL экземпляра Discourse (без завершающего слэша)
DISCOURSE_URL = 'https://yourcommunity.com'

# Административный API-ключ (должен иметь права на имитацию)
API_KEY = 'YOUR_API_KEY_HERE'

# Имя пользователя администратора, которому принадлежит API-ключ
API_USERNAME = 'system'

# Путь к CSV-файлу со столбцами topic_id и username
CSV_FILE = 'topics_to_vote.csv'

# Режим сухой проверки — установите False, чтобы реально выставлять голоса
DRY_RUN = True

# Задержка между вызовами API (секунды) для избежания ограничений скорости
DELAY_BETWEEN_REQUESTS = 0.5

#==============================================================================
# СКРИПТ
#==============================================================================

def cast_vote(topic_id: int, username: str) -> dict:
    """
    Выставить голос за тему от имени конкретного пользователя.

    Аргументы:
        topic_id: ID темы, за которую нужно проголосовать
        username: Имя пользователя для имитации при голосовании

    Возвращает:
        Словарь с булевым значением 'success' и строкой 'message'
    """
    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': 'Голос успешно выставлен'}
        elif response.status_code == 422:
            # Обычно означает, что уже проголосовали или голосование не включено
            return {'success': False, 'message': 'Уже проголосовали или голосование не включено'}
        elif response.status_code == 403:
            return {'success': False, 'message': 'Отказано в доступе — проверьте права API-ключа'}
        elif response.status_code == 404:
            return {'success': False, 'message': 'Тема не найдена или голосование не включено в категории'}
        else:
            return {'success': False, 'message': f'HTTP {response.status_code}: {response.text[:200]}'}

    except requests.RequestException as e:
        return {'success': False, 'message': f'Ошибка запроса: {str(e)}'}


def main():
    print("\n" + "=" * 60)
    print("Заполнение самоголосования через API")
    print("=" * 60)
    print(f"Режим: {'СУХАЯ ПРОВЕРКА (без изменений)' if DRY_RUN else 'ЖИВОЙ (голоса будут выставлены)'}")
    print(f"Цель: {DISCOURSE_URL}")
    print(f"CSV-файл: {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"ОШИБКА: CSV-файл не найден: {CSV_FILE}")
        print("\nСоздайте CSV-файл со следующим форматом:")
        print("topic_id,username")
        print("12345,john_doe")
        print("12346,jane_smith")
        return
    except Exception as e:
        print(f"ОШИБКА: Не удалось прочитать CSV-файл: {e}")
        return

    if not rows:
        print("ОШИБКА: CSV-файл пуст")
        return

    # Проверка столбцов CSV
    columns = set(rows[0].keys())

    if 'topic_id' not in columns or 'username' not in columns:
        print(f"ОШИБКА: CSV должен содержать столбцы: topic_id и username")
        print(f"Найденные столбцы: {columns}")
        return

    print(f"Найдено тем для обработки: {len(rows)}\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_id} от @{username}", end=" ")

        if DRY_RUN:
            print("-> было бы выставлено")
            success_count += 1
        else:
            result = cast_vote(int(topic_id), username)

            if result['success']:
                print("-> голос выставлен!")
                success_count += 1
            elif 'Уже проголосовали' in result['message']:
                print("-> уже проголосовали (пропущено)")
                skip_count += 1
            else:
                print(f"-> ОШИБКА: {result['message']}")
                error_count += 1

            # Ограничение скорости
            if i < len(rows):
                time.sleep(DELAY_BETWEEN_REQUESTS)

    # Итоги
    print("\n" + "=" * 60)
    print("ИТОГИ")
    print("=" * 60)
    print(f"Всего тем: {len(rows)}")
    print(f"Голосов {'должно быть выставлено' if DRY_RUN else 'выставлено'}: {success_count}")
    print(f"Уже проголосовали (пропущено): {skip_count}")
    print(f"Ошибки: {error_count}")

    if DRY_RUN:
        print("\n** СУХАЯ ПРОВЕРКА ЗАВЕРШЕНА **")
        print("Чтобы выставить голоса, установите DRY_RUN = False и запустите снова.")

    print("")


if __name__ == '__main__':
    main()
4 лайка