Implement LDAP avatar serving #1
|
@ -187,6 +187,73 @@ files = [
|
||||||
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
|
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "10.0.1"
|
||||||
|
description = "Python Imaging Library (Fork)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"},
|
||||||
|
{file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"},
|
||||||
|
{file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"},
|
||||||
|
{file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"},
|
||||||
|
{file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"},
|
||||||
|
{file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"},
|
||||||
|
{file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"},
|
||||||
|
{file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"},
|
||||||
|
{file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"},
|
||||||
|
{file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"},
|
||||||
|
{file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"},
|
||||||
|
{file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"},
|
||||||
|
{file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"},
|
||||||
|
{file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"},
|
||||||
|
{file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||||
|
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -273,4 +340,4 @@ email = ["email-validator"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "~3.11"
|
python-versions = "~3.11"
|
||||||
content-hash = "b026e5d2d31ebde4256e7793a62fbba895b049cfc270bf24ab1a0ed352f9d933"
|
content-hash = "5fe83018dde9d22d0820c713b674849afb60d077ad33429fcfa330cafcba4ef0"
|
||||||
|
|
|
@ -13,6 +13,7 @@ itsdangerous = "^2.0"
|
||||||
Jinja2 = "^3.0"
|
Jinja2 = "^3.0"
|
||||||
kerberos = "^1.3.0"
|
kerberos = "^1.3.0"
|
||||||
MarkupSafe = "^2.0"
|
MarkupSafe = "^2.0"
|
||||||
|
pillow = "^10.0.1"
|
||||||
python-ldap = "^3.2.0"
|
python-ldap = "^3.2.0"
|
||||||
uWSGI = "^2.0"
|
uWSGI = "^2.0"
|
||||||
Werkzeug = "^2.0"
|
Werkzeug = "^2.0"
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
|
@ -45,8 +45,8 @@ def start():
|
||||||
validation.sanitize_readable()
|
validation.sanitize_readable()
|
||||||
|
|
||||||
from webapp import views
|
from webapp import views
|
||||||
from webapp import auth, admin, vcard, passwd
|
from webapp import auth, admin, avatar, vcard, passwd
|
||||||
for module in (auth, admin, vcard, passwd):
|
for module in (auth, admin, avatar, vcard, passwd):
|
||||||
app.register_blueprint(module.bp)
|
app.register_blueprint(module.bp)
|
||||||
|
|
||||||
app.connections = pools.LDAPConnectionPool(config.ldap_url, timeout=300.0)
|
app.connections = pools.LDAPConnectionPool(config.ldap_url, timeout=300.0)
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
"""
|
||||||
|
Module which serves users' avatars.
|
||||||
|
|
||||||
|
Based on a simple cache which keeps all avatars in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import colorsys
|
||||||
|
import time
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
import flask
|
||||||
|
import ldap
|
||||||
|
|
||||||
|
from webapp import context, ldaputils
|
||||||
|
|
||||||
|
bp = flask.Blueprint('avatar', __name__)
|
||||||
|
log = logging.getLogger('ldap-web.avatar')
|
||||||
|
|
||||||
|
|
||||||
|
# Stolen from https://stackoverflow.com/questions/43512615/reshaping-rectangular-image-to-square
|
||||||
|
def resize_image(image: Image, length: int) -> Image:
|
||||||
|
"""
|
||||||
|
Resize an image to a square. Can make an image bigger to make it fit or smaller if it doesn't fit. It also crops
|
||||||
|
part of the image.
|
||||||
|
|
||||||
|
:param self:
|
||||||
|
:param image: Image to resize.
|
||||||
|
:param length: Width and height of the output image.
|
||||||
|
:return: Return the resized image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
Resizing strategy :
|
||||||
|
1) We resize the smallest side to the desired dimension (e.g. 1080)
|
||||||
|
2) We crop the other side so as to make it fit with the same length as the smallest side (e.g. 1080)
|
||||||
|
"""
|
||||||
|
if image.size[0] < image.size[1]:
|
||||||
|
# The image is in portrait mode. Height is bigger than width.
|
||||||
|
|
||||||
|
# This makes the width fit the LENGTH in pixels while conserving the ration.
|
||||||
|
resized_image = image.resize((length, int(image.size[1] * (length / image.size[0]))))
|
||||||
|
|
||||||
|
# Amount of pixel to lose in total on the height of the image.
|
||||||
|
required_loss = (resized_image.size[1] - length)
|
||||||
|
|
||||||
|
# Crop the height of the image so as to keep the center part.
|
||||||
|
resized_image = resized_image.crop(
|
||||||
|
box=(0, required_loss / 2, length, resized_image.size[1] - required_loss / 2))
|
||||||
|
|
||||||
|
# We now have a length*length pixels image.
|
||||||
|
return resized_image
|
||||||
|
else:
|
||||||
|
# This image is in landscape mode or already squared. The width is bigger than the heihgt.
|
||||||
|
|
||||||
|
# This makes the height fit the LENGTH in pixels while conserving the ration.
|
||||||
|
resized_image = image.resize((int(image.size[0] * (length / image.size[1])), length))
|
||||||
|
|
||||||
|
# Amount of pixel to lose in total on the width of the image.
|
||||||
|
required_loss = resized_image.size[0] - length
|
||||||
|
|
||||||
|
# Crop the width of the image so as to keep 1080 pixels of the center part.
|
||||||
|
resized_image = resized_image.crop(
|
||||||
|
box=(required_loss / 2, 0, resized_image.size[0] - required_loss / 2, length))
|
||||||
|
|
||||||
|
# We now have a length*length pixels image.
|
||||||
|
return resized_image
|
||||||
|
|
||||||
|
|
||||||
|
syrenka = Image.open("syrenka.png")
|
||||||
|
|
||||||
|
|
||||||
|
def default_avatar(uid: str) -> Image:
|
||||||
|
"""
|
||||||
|
Create little generative avatar for people who don't have a custom one
|
||||||
|
configured.
|
||||||
|
"""
|
||||||
|
img = Image.new('RGBA', (256, 256), (255, 255, 255, 0))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Deterministic rng for stable output.
|
||||||
|
rng = random.Random(uid)
|
||||||
|
|
||||||
|
# Pick a nice random neon color.
|
||||||
|
n_h, n_s, n_l = rng.random(), 0.5 + rng.random()/2.0, 0.4 + rng.random()/5.0
|
||||||
|
|
||||||
|
# Use muted version for background.
|
||||||
|
r, g, b = [int(256*i) for i in colorsys.hls_to_rgb(n_h, n_l+0.3, n_s-0.1)]
|
||||||
|
draw.rectangle([(0, 0), (256, 256)], fill=(r,g,b,255))
|
||||||
|
|
||||||
|
# Scale logo by randomized factor.
|
||||||
|
factor = 0.7 + 0.1 * rng.random()
|
||||||
|
w, h = int(syrenka.size[0] * factor), int(syrenka.size[1] * factor)
|
||||||
|
overlay = syrenka.resize((w, h))
|
||||||
|
|
||||||
|
# Crop to headshot.
|
||||||
|
overlay = overlay.crop(box=(0, 0, w, w))
|
||||||
|
|
||||||
|
# Give it a little nudge.
|
||||||
|
overlay = overlay.rotate((rng.random() - 0.5) * 100)
|
||||||
|
|
||||||
|
# Colorize with full neon color.
|
||||||
|
r, g, b = [int(256*i) for i in colorsys.hls_to_rgb(n_h,n_l,n_s)]
|
||||||
|
pixels = overlay.load()
|
||||||
|
for x in range(img.size[0]):
|
||||||
|
for y in range(img.size[1]):
|
||||||
|
alpha = pixels[x, y][3]
|
||||||
|
pixels[x, y] = (r, g, b, alpha)
|
||||||
|
|
||||||
|
img.alpha_composite(overlay)
|
||||||
|
|
||||||
|
res = io.BytesIO()
|
||||||
|
img.save(res, 'PNG')
|
||||||
|
return res.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarCacheEntry:
|
||||||
|
# UID of the avatar's user.
|
||||||
|
uid: str
|
||||||
|
# Deadline when this entry expires.
|
||||||
|
deadline: float
|
||||||
|
# Cached source bytes
|
||||||
|
data: bytes
|
||||||
|
# Cached converted bytes
|
||||||
|
_converted: bytes
|
||||||
|
|
||||||
|
def __init__(self, uid: str, data: bytes):
|
||||||
|
self.uid = uid
|
||||||
|
self.deadline = time.time() + 3600
|
||||||
|
self.data = data
|
||||||
|
self._converted = b""
|
||||||
|
|
||||||
|
def serve(self):
|
||||||
|
"""
|
||||||
|
Serve sanitized image. Always re-encode to PNG 256x256.
|
||||||
|
"""
|
||||||
|
# Re-encode to PNG if we haven't yet.
|
||||||
|
if self._converted == b"":
|
||||||
|
try:
|
||||||
|
img = Image.open(io.BytesIO(self.data))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Could not parse avatar for {}: {}".format(self.uid, e))
|
||||||
|
self.data = default_avatar(self.uid)
|
||||||
|
img = Image.open(io.BytesIO(self.data))
|
||||||
|
|
||||||
|
res = io.BytesIO()
|
||||||
|
img = resize_image(img, 256)
|
||||||
|
img.save(res, 'PNG')
|
||||||
|
self._converted = res.getvalue()
|
||||||
|
|
||||||
|
return flask.Response(self._converted, mimetype='image/png')
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarCache:
|
||||||
|
# keyed by uid
|
||||||
|
entries: dict[str, AvatarCacheEntry]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.entries = {}
|
||||||
|
|
||||||
|
def get(self, uid: str, bust: bool = False) -> AvatarCacheEntry:
|
||||||
|
"""
|
||||||
|
Get avatar, either from cache or from LDAP on cache miss. If 'bust' is
|
||||||
|
set to True, the cache will not be consulted and the newest result from
|
||||||
|
LDAP will always be served.
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
# Try to get a cache entry to serve, if possible.
|
||||||
|
if not bust and uid in self.entries:
|
||||||
|
entry = self.entries[uid]
|
||||||
|
if entry.deadline > now:
|
||||||
|
return entry.serve()
|
||||||
|
|
||||||
|
# Otherwise, retrieve from LDAP.
|
||||||
|
conn = context.get_admin_connection()
|
||||||
|
try:
|
||||||
|
dn = ldaputils.user_dn(uid)
|
||||||
|
res = conn.search_s(dn, ldap.SCOPE_SUBTREE)
|
||||||
|
except ldap.NO_SUCH_OBJECT:
|
||||||
|
res = []
|
||||||
|
|
||||||
|
avatar = None
|
||||||
|
if len(res) == 1:
|
||||||
|
for attr, vs in res[0][1].items():
|
||||||
|
if attr == 'jpegPhoto':
|
||||||
|
for v in vs:
|
||||||
|
try:
|
||||||
|
avatar = base64.b64decode(v)
|
||||||
|
except binascii.Error as e:
|
||||||
|
log.warning("Could not b64decode avatar for {}".format(uid))
|
||||||
|
avatar = None
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
break
|
||||||
|
# If nothing was found in LDAP (either uid doesn't exist or uid doesn't
|
||||||
|
# have an avatar attached), serve default avatar.
|
||||||
|
if avatar is None:
|
||||||
|
avatar = default_avatar(uid)
|
||||||
|
|
||||||
|
# Save avatar in cache.
|
||||||
|
entry = AvatarCacheEntry(uid, avatar)
|
||||||
|
self.entries[uid] = entry
|
||||||
|
|
||||||
|
# And serve the entry.
|
||||||
|
return entry.serve()
|
||||||
|
|
||||||
|
cache = AvatarCache()
|
||||||
|
|
||||||
|
@bp.route('/avatar/<uid>', methods=['GET'])
|
||||||
|
def avatar_serve(uid):
|
||||||
|
return cache.get(uid)
|
Loading…
Reference in New Issue