Создайте пользовательский бейдж с изображением через API

Конечно, включённые значки — это… мило. С ними всё в порядке. Но что, если вы хотите большего? Что, если вы хотите выйти за пределы предопределённого набора символов? Конечно, есть административная страница, где можно их загружать. Но что, если вы хотите создать целый ворох значков?

Что ж, хорошие новости! Это можно сделать через API. Вот код на Python, показывающий, как это работает. (И в качестве бонуса он выдаёт значок в конце.)

Этот код должен быть довольно понятным, даже если вы не программист на Python. Вы даже можете воспроизвести его с помощью curl в командной строке, если хотите.

Ожидается, что ваш API-ключ будет храниться в переменной окружения DISCOURSE_API_KEY.

#!/usr/bin/python3

import os         # Для чтения переменных окружения
import sys        # Для выхода. :)
import hashlib    # Для хэширования изображения

import requests   # Выполняет основную работу


# Настройки сайта и аутентификации
DISCOURSE = "discussion.fedoraproject.org"
DISCOURSE_API_USER = "mattdm"
DISCOURSE_API_KEY = os.getenv("DISCOURSE_API_KEY")
if not DISCOURSE_API_KEY:
    print(f"Ошибка: переменная окружения DISCOURSE_API_KEY должна быть установлена", file=sys.stderr)
    sys.exit(2)

# Информация о вашем значке. В реальном сценарии, вероятно, вы не будете хардкодить эти данные.
NAME = "Apex"
IMAGE = "apex.png"
DESCRIPTION = "Благословение FPL"
MORE = "Вы потрясающий, и все должны это знать!"
TARGET_USER = "mattdm"

# Наши заголовки аутентификации, как указано выше.
HEADERS = {'Api-Key': DISCOURSE_API_KEY, 'Api-Username': DISCOURSE_API_USER}

# С помощью этого запроса можно получить ID и описания групп значков,
# а также различные типы значков. Если захотите. В этом примере мы это не используем.
r = requests.get(f"https://{DISCOURSE}/admin/badges.json", headers=HEADERS)
if r.status_code != 200:
    print(f"Ошибка при получении списка значков: получен код {r.status_code}", file=sys.stderr)
    sys.exit(1)

# Проверка, что имя ещё не существует.
# Вероятно, есть лучший способ сделать это, но для примера сойдёт.
if NAME in list(map(lambda x: x['name'], r.json()['badges'])):
    print(f"Ошибка: значок '{NAME}' уже существует.")
    sys.exit(3)

# Чтение изображения. В реальном коде здесь нужна более качественная обработка ошибок!
with open(IMAGE, "rb") as f:
    image_data = f.read()

# Здесь, вероятно, стоит добавить проверку размера и размеров изображения....

# Но в любом случае, собираем пакет информации для загрузки.
# Единственная сложность здесь — получение контрольной суммы изображения.
file_info = {'file_name': f'{IMAGE}',
             'file_size': f'{len(image_data)}',
             'type': 'badge_image',
             'metadata[sha1-checksum]': hashlib.sha1(image_data).hexdigest(),
             }


# И спрашиваем у Discourse, куда его отправить.
r = requests.post(
    f"https://{DISCOURSE}/uploads/generate-presigned-put", json=file_info, headers=HEADERS)
if r.status_code != 200:
    print(
        f"Ошибка при запросе места для загрузки изображения: получен код {r.status_code}", file=sys.stderr)
    sys.exit(1)

upload_url = r.json()['url']
upload_uid = r.json()['unique_identifier']

# Теперь загружаем его туда, куда нам сказали.
r = requests.put(upload_url, data=image_data)
if r.status_code != 200:
    print(
        f"Ошибка при загрузке изображения во внешнее хранилище: получен код {r.status_code}", file=sys.stderr)
    sys.exit(1)

# И сообщаем Discourse, что всё прошло успешно, получив ID, на который можно ссылаться позже.
r = requests.post(f"https://{DISCOURSE}/uploads/complete-external-upload",
                  data=f'unique_identifier={upload_uid}', headers=HEADERS)
if r.status_code != 200:
    print(f"Ошибка при завершении загрузки: получен код {r.status_code}", file=sys.stderr)
    sys.exit(1)
image_id = r.json()['id']


# Примечание: если вы хотите использовать Font Awesome, оставьте `image_upload_id` пустым
# и вместо этого установите `icon` в соответствующее имя иконки Font Awesome. И, конечно,
# в этом случае можно пропустить всю загрузку изображения!
badge = {'allow_title': 'true',
         'multiple_grant': 'false',
         'listable': 'true',
         'show_posts': 'false',
         'target_posts': 'false',
         'name': f'{NAME}',
         'description': f'{DESCRIPTION}',
         'long_description': f'{DESCRIPTION} {MORE}',
         'image_upload_id': f'{image_id}',
         'badge_grouping_id': '5',
         'badge_type_id': '1',
         }


r = requests.post(
    f"https://{DISCOURSE}/admin/badges.json", json=badge, headers=HEADERS)
if r.status_code != 200:
    print(f"Ошибка при создании значка: получен код {r.status_code}", file=sys.stderr)
    sys.exit(1)

badge_id = r.json()['badge']['id']

print(f'Значок "{NAME}: {DESCRIPTION}" создан с ID {badge_id}!')


# И для полноты картины выдадим значок!

# Вы также можете добавить "причину", которая должна ссылаться на пост или тему.
bestow = {'username': f"{TARGET_USER}", 'badge_id': f'{badge_id}'}

r = requests.post(f"https://{DISCOURSE}/user_badges",
                  json=bestow, headers=HEADERS)
if r.status_code != 200:
    print(f"Ошибка при выдаче значка: получен код {r.status_code}", file=sys.stderr)
    sys.exit(1)

print(f'Пользователю {TARGET_USER} выдан новый значок!')
7 лайков

К сожалению, для этого требуется глобальный ключ API администратора. Я надеюсь, что команда внедрит возможность проведения детальных проверок прав на загрузку, создание и редактирование значков, назначение значков и удаление значков.

Теперь для этого больше не требуется глобальный ключ API администратора — при создании ключа API вы можете указать конкретные действия, связанные с созданием, предоставлением или удалением значков. Не забудьте включить разрешение на “загрузку”, если вы хотите выполнить действия, показанные в этом примере, и добавить свои изображения. Это разрешение находится в отдельном разделе от разрешений API для значков.

2 лайка

В оригинальном посте я случайно поместил функцию хеширования sha внутрь строки, из-за чего она была записана буквально как код Python, а не как хеш. Это вроде бы работало, но вероятно, должно быть исправлено. Не уверен, кто именно проверяет это, но, скорее всего, если сделать это неправильно, изображение в какой-то момент будет помечено как повреждённое. В любом случае, я исправил код выше, теперь он должен быть правильным. Если вы заметите какие-либо другие ошибки, пожалуйста, дайте мне знать!

3 лайка