commit
34d897ff78
|
@ -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/<uid>', 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/<email_hash>', 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/<uid>', methods=['GET'])
|
||||
def avatar_serve(uid):
|
||||
return cache.get(uid)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,7 +4,7 @@ from webapp import config
|
|||
|
||||
def is_valid_name(name):
|
||||
"""`true` if `name` is a safe ldap uid/cn"""
|
||||
return re.match(r'^[a-zA-Z_][a-zA-Z0-9-_]*\Z', name) is not None
|
||||
return re.match(r'^[a-zA-Z_][a-zA-Z0-9-_\.]*\Z', name) is not None
|
||||
|
||||
def validate_name(name):
|
||||
"""Raises `RuntimeError` if `name` is not a safe ldap uid/cn"""
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% extends 'basic.html' %}
|
||||
{% block content %}
|
||||
<img src="/avatar/user/{{ uid }}" class="profile-avatar-lmao" />
|
||||
|
||||
<h1>User: {{ uid }}</h1>
|
||||
|
||||
<div style="margin-bottom: 10px">
|
||||
|
@ -27,7 +29,7 @@
|
|||
<tr>
|
||||
<td>{{ attr }}</td>
|
||||
<td>{{ attr_readable if attr_readable else '' }}</td>
|
||||
<td class="profile-table-value">{{ value }}</td>
|
||||
<td class="profile-table-value">{{ '(...omitted...)' if attr == 'jpegPhoto' else value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
@ -45,5 +47,17 @@
|
|||
.profile-table th.profile-table-value {
|
||||
width: max-content;
|
||||
}
|
||||
.profile-avatar-lmao {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 10px 0;
|
||||
border-radius: 10px;
|
||||
float: right;
|
||||
box-shadow: 5px 5px 0 #ddd;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.profile-avatar-lmao:hover {
|
||||
transform: scale(1.2) rotate(359deg);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<div class="col-md-{{width}}" style="margin-bottom: 20px;">
|
||||
<div style="background-color: #eee; padding: 10px 30px; border-radius: 12px; display: flex; flex-direction: row; gap: 20px">
|
||||
<div>
|
||||
<img src="/avatar/{{ profile.uid[0] }}" style="width: 70px; height: 70px; margin: 10px 0" />
|
||||
<img src="/avatar/user/{{ profile.uid[0] }}" style="width: 70px; height: 70px; margin: 10px 0" />
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ name|readable }}</h4>
|
||||
|
|
Loading…
Reference in New Issue