From 6f51489194020965d0efcdaa814a81ff34ea4f5e Mon Sep 17 00:00:00 2001 From: radex Date: Mon, 23 Oct 2023 19:48:00 +0200 Subject: [PATCH] Add Gravatar-style avatar endpoint, rename user-based requests to /avatar/user/ --- webapp/avatar.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++- webapp/context.py | 1 + 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/webapp/avatar.py b/webapp/avatar.py index 8ac1edd..2a7fefc 100644 --- a/webapp/avatar.py +++ b/webapp/avatar.py @@ -11,6 +11,7 @@ import time import io import logging import random +import hashlib from PIL import Image, ImageDraw import flask @@ -234,6 +235,68 @@ class AvatarCache: cache = AvatarCache() -@bp.route('/avatar/', methods=['GET']) +def hash_for_uid(uid): + # NOTE: Gravatar documentation says to use SHA256, but everyone passes MD5 instead + email = f'{uid}@hackerspace.pl'.strip().lower() + hasher = hashlib.md5() + hasher.update(email.encode()) + return hasher.hexdigest() + +def get_all_user_uids(conn): + all_uids = [] + + results = conn.search_s(config.ldap_people, ldap.SCOPE_SUBTREE, 'uid=*', attrlist=['uid']) + for user, attrs in results: + uid = attrs['uid'][0].decode() + all_uids.append(uid) + + return all_uids + +class HashCache: + # email hash -> uid mapping + entries: dict[str, str] = {} + # deadline when this cache expires + deadline: float = 0 + + def get(self, email_hash: str) -> str: + self.rebuild_if_needed() + return self.entries.get(email_hash, 'default') + + def reset(self): + self.entries = {} + self.deadline = 0 + + def rebuild_if_needed(self): + now = time.time() + if now > self.deadline: + self.rebuild() + + def rebuild(self): + log.info("Rebuilding email hash cache") + conn = context.get_admin_connection() + users = get_all_user_uids(conn) + self.deadline = time.time() + config.avatar_cache_timeout + self.entries = { hash_for_uid(uid): uid for uid in users } + +hash_cache = HashCache() + +def sanitize_email_hash(hash: str): + """ + lowercases, removes file extension (probably) + """ + hash = hash.lower() + if hash.endswith('.png') or hash.endswith('.jpg'): + hash = hash[:-4] + return hash + +@bp.route('/avatar/', methods=['GET']) +def gravatar_serve(email_hash): + """ + Serves avatar in a Gravatar-compatible(ish) way, i.e. by email hash, not user name. + """ + uid = hash_cache.get(sanitize_email_hash(email_hash)) + return cache.get(uid) + +@bp.route('/avatar/user/', methods=['GET']) def avatar_serve(uid): return cache.get(uid) diff --git a/webapp/context.py b/webapp/context.py index f09b9c6..e1f8d29 100644 --- a/webapp/context.py +++ b/webapp/context.py @@ -52,5 +52,6 @@ def refresh_profile(dn=None): # bust avatar cache if user_uid: avatar.cache.reset_user(user_uid) + avatar.hash_cache.reset() return profile