Create a custom badge with an image through the API

Sure, the included badges are … nice. Nothing wrong with them at all. But, what if you want more? What if you want to go beyond the pre-defined symbol set? Sure, there’s an admin page where you can upload them. But what if you want to make a whole lot of badges?

Well, good news! You can do this through the API. Here’s Python code showing how it works. (And it awards the badge at the end, for good measure.)

This should be pretty easy to read even if you’re not a Python programmer. You can even replicate it with curl on the command line, if you like.

This expects your API key to be in an environment variable DISCOURSE_API_KEY.

#!/usr/bin/python3

import os         # For reading the environment
import sys        # For exiting. :)
import hashlib    # For the image hash

import requests   # Does all the real work


# Site and auth stuff
DISCOURSE = "discussion.fedoraproject.org"
DISCOURSE_API_USER = "mattdm"
DISCOURSE_API_KEY = os.getenv("DISCOURSE_API_KEY")
if not DISCOURSE_API_KEY:
    print(f"Error: DISCOURSE_API_KEY must be set in the environment", file=sys.stderr)
    sys.exit(2)

# Info for your badge. Presumably, in real use you would not hard code this.
NAME = "Apex"
IMAGE = "apex.png"
DESCRIPTION = "Blessing of the FPL"
MORE = "You are awesome and everyone should know!"
TARGET_USER = "mattdm"

# Our auth headers, from above.
HEADERS = {'Api-Key': DISCOURSE_API_KEY, 'Api-Username': DISCOURSE_API_USER}

# From this, you can get the ids and descriptions of badge groups, and
# the various badges types. If you want. Not using that in this example.
r = requests.get(f"https://{DISCOURSE}/admin/badges.json", headers=HEADERS)
if r.status_code != 200:
    print(f"Error getting badge list: got {r.status_code}", file=sys.stderr)
    sys.exit(1)

# Check that the name doesn't already exist.
# There's probably a better way to do this, but it'll suffice.
if NAME in list(map(lambda x: x['name'], r.json()['badges'])):
    print(f"Error: badge '{NAME}' already exists.")
    sys.exit(3)

# Read the image. You'd want better error handling here in real code!
with open(IMAGE, "rb") as f:
    image_data = f.read()

# Probably should do some sanity checking on the
# image file size/dimensions right about here....

# But anyway, assemble a packet of information about it to upload.
# The only tricky thing here really is getting the image checksum.
file_info = {'file_name': f'{IMAGE}',
             'file_size': f'{len(image_data)}',
             'type': 'badge_image',
             'metadata[sha1-checksum]': hashlib.sha1(image_data).hexdigest(),
             }


# And ask Discourse where to send it.
r = requests.post(
    f"https://{DISCOURSE}/uploads/generate-presigned-put", json=file_info, headers=HEADERS)
if r.status_code != 200:
    print(
        f"Error asking where to upload the image: got {r.status_code}", file=sys.stderr)
    sys.exit(1)

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

# Now put it where we were told to.
r = requests.put(upload_url, data=image_data)
if r.status_code != 200:
    print(
        f"Error uploading image to external storage: got {r.status_code}", file=sys.stderr)
    sys.exit(1)

# And tell Discourse that it worked, and get back an id we can reference later.
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"Error completing upload: got {r.status_code}", file=sys.stderr)
    sys.exit(1)
image_id = r.json()['id']


# Note: if you want to use font awesome, leave `image_upload_id` blank and
# instead set `icon` to the corresponding font awesome name.And of course
# you can skip all the image upload stuff in that case!
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"Error creating badge: got {r.status_code}", file=sys.stderr)
    sys.exit(1)

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

print(f'Badge "{NAME}: {DESCRIPTION}" has been created as {badge_id}!')


# And for completeness, grant the badge!

# You can also add a "reason", which must link to a post or topic.
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"Error granting badge: got {r.status_code}", file=sys.stderr)
    sys.exit(1)

print(f'User {TARGET_USER} has been awarded the new badge!')

7 Likes

Unfortunately you need a global admin API key for this. I’m hoping the team will implement a way to make granular checks for upload rights, badge creation and editing, badge assignment, and badge removal.

You don’t need a global admin API key for this anymore — when creating an API key, you can specify different specific things related to creating, granting, or destroying badges. Don’t forget “upload” if you want to do what this example shows and add your own images. That’s in a separate section from the badges API permissions.

2 Likes

In the original posting of this, I accidentally put the sha hash digest function inside a string, so that got set literally as the python code, not the digest. That seemed to still work, but probably should actually be right instead. I’m not sure what is checking that, but probably if you do this wrong the image will get marked as corrupt some point in the future. Anyway, I’ve edited the code above so it should be right. If you spot any other bugs, please let me know!

3 Likes