From 53b2117f542a3f5df9bdf19284d0296180496e02 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Sat, 24 Jun 2023 19:31:10 +0000 Subject: [PATCH] users: Move avatar handler Signed-off-by: Michael Tremer --- src/web/__init__.py | 4 +- src/web/people.py | 105 ----------------------------------------- src/web/users.py | 112 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 107 deletions(-) diff --git a/src/web/__init__.py b/src/web/__init__.py index 9372efde..9c0480aa 100644 --- a/src/web/__init__.py +++ b/src/web/__init__.py @@ -152,7 +152,8 @@ class Application(tornado.web.Application): # Users (r"/users", users.IndexHandler), - (r"/users/([\w]+)", users.ShowHandler), + (r"/users/([a-z_][a-z0-9_-]{0,31})", users.ShowHandler), + (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", users.AvatarHandler), # RSS feed (r"/news.rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }), @@ -304,7 +305,6 @@ class Application(tornado.web.Application): (r"/groups/([a-z_][a-z0-9_-]{0,31})", people.GroupHandler), (r"/register", auth.RegisterHandler), (r"/search", people.SearchHandler), - (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", people.AvatarHandler), (r"/users/([a-z_][a-z0-9_-]{0,31})/calls/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", people.CallHandler), (r"/users/([a-z_][a-z0-9_-]{0,31})/calls(?:/(\d{4}-\d{2}-\d{2}))?", people.CallsHandler), (r"/users/([a-z_][a-z0-9_-]{0,31})/edit", people.UserEditHandler), diff --git a/src/web/people.py b/src/web/people.py index 1124dda7..077240a7 100644 --- a/src/web/people.py +++ b/src/web/people.py @@ -1,12 +1,7 @@ #!/usr/bin/python -import PIL import datetime -import imghdr -import io import ldap -import logging -import os.path import tornado.web import urllib.parse @@ -16,112 +11,12 @@ from . import auth from . import base from . import ui_modules -COLOUR_LIGHT = (237,232,232) -COLOUR_DARK = (49,53,60) - class IndexHandler(auth.CacheMixin, base.BaseHandler): @tornado.web.authenticated def get(self): self.render("people/index.html") -class AvatarHandler(base.BaseHandler): - def get(self, uid): - # Get the desired size of the avatar file - size = self.get_argument("size", None) - - try: - size = int(size) - except (TypeError, ValueError): - size = None - - logging.debug("Querying for avatar of %s" % uid) - - # Fetch user account - account = self.backend.accounts.get_by_uid(uid) - if not account: - raise tornado.web.HTTPError(404, "Could not find account %s" % uid) - - # Allow downstream to cache this for a year - self.set_expires(31536000) - - # Resize avatar - avatar = account.get_avatar(size) - - # If there is no avatar, we serve a default image - if not avatar: - logging.debug("No avatar uploaded for %s" % account) - - # Generate a random avatar with only one letter - avatar = self._get_avatar(account, size=size) - - # Guess content type - type = imghdr.what(None, avatar) - - # Set headers about content - self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type)) - self.set_header("Content-Type", "image/%s" % type) - - # Deliver payload - self.finish(avatar) - - def _get_avatar(self, account, size=None, **args): - letter = ("%s" % account)[0].upper() - - if size is None: - size = 256 - - # The generated avatar cannot be larger than 1024px - if size >= 2048: - size = 2048 - - # Cache key - key = "avatar:letter:%s:%s" % (letter, size) - - # Fetch avatar from the cache - avatar = self.memcached.get(key) - if not avatar: - avatar = self._make_avatar(letter, size=size, **args) - - # Cache for forever - self.memcached.set(key, avatar) - - return avatar - - def _make_avatar(self, letter, format="PNG", size=None, **args): - # Generate an image of the correct size - image = PIL.Image.new("RGBA", (size, size), COLOUR_LIGHT) - - # Have a canvas - draw = PIL.ImageDraw.Draw(image) - - # Load font - font = PIL.ImageFont.truetype(os.path.join( - self.application.settings.get("static_path", ""), - "fonts/Prompt-Bold.ttf" - ), size, encoding="unic") - - # Determine size of the printed letter - w, h = font.getsize(letter) - - # Mukta seems to be very broken and the height needs to be corrected - h //= 0.7 - - # Draw the letter in the center - draw.text(((size - w) / 2, (size - h) / 2), letter, - font=font, fill=COLOUR_DARK) - - with io.BytesIO() as f: - # If writing out the image does not work with optimization, - # we try to write it out without any optimization. - try: - image.save(f, format, optimize=True, **args) - except: - image.save(f, format, **args) - - return f.getvalue() - - class CallsHandler(auth.CacheMixin, base.BaseHandler): @tornado.web.authenticated def get(self, uid, date=None): diff --git a/src/web/users.py b/src/web/users.py index 74b4d0a5..9b12e624 100644 --- a/src/web/users.py +++ b/src/web/users.py @@ -1,11 +1,19 @@ #!/usr/bin/python +import PIL +import imghdr +import io +import logging +import os.path import tornado.web from . import auth from . import base from . import ui_modules +COLOUR_LIGHT = (237,232,232) +COLOUR_DARK = (49,53,60) + class IndexHandler(auth.CacheMixin, base.BaseHandler): @tornado.web.authenticated def get(self): @@ -31,6 +39,110 @@ class ShowHandler(auth.CacheMixin, base.BaseHandler): self.render("users/show.html", account=account) +class AvatarHandler(base.BaseHandler): + def get(self, uid): + # Get the desired size of the avatar file + size = self.get_argument("size", None) + + try: + size = int(size) + except (TypeError, ValueError): + size = None + + logging.debug("Querying for avatar of %s" % uid) + + # Fetch user account + account = self.backend.accounts.get_by_uid(uid) + if not account: + raise tornado.web.HTTPError(404, "Could not find account %s" % uid) + + # Allow downstream to cache this for a year + self.set_expires(31536000) + + # Resize avatar + avatar = account.get_avatar(size) + + # If there is no avatar, we serve a default image + if not avatar: + logging.debug("No avatar uploaded for %s" % account) + + # Generate a random avatar with only one letter + avatar = self._get_avatar(account, size=size) + + # Guess content type + type = imghdr.what(None, avatar) + + # If we could not guess the type, we will try something else + if not type: + # Could this be an SVG file? + if avatar.startswith(b"<"): + type = "svg+xml" + + # Set headers about content + self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type)) + if type: + self.set_header("Content-Type", "image/%s" % type) + + # Deliver payload + self.finish(avatar) + + def _get_avatar(self, account, size=None, **args): + letter = ("%s" % account)[0].upper() + + if size is None: + size = 256 + + # The generated avatar cannot be larger than 1024px + if size >= 2048: + size = 2048 + + # Cache key + key = "avatar:letter:%s:%s" % (letter, size) + + # Fetch avatar from the cache + avatar = self.memcached.get(key) + if not avatar: + avatar = self._make_avatar(letter, size=size, **args) + + # Cache for forever + self.memcached.set(key, avatar) + + return avatar + + def _make_avatar(self, letter, format="PNG", size=None, **args): + # Generate an image of the correct size + image = PIL.Image.new("RGBA", (size, size), COLOUR_LIGHT) + + # Have a canvas + draw = PIL.ImageDraw.Draw(image) + + # Load font + font = PIL.ImageFont.truetype(os.path.join( + self.application.settings.get("static_path", ""), + "fonts/Prompt-Bold.ttf" + ), size, encoding="unic") + + # Determine size of the printed letter + w, h = font.getsize(letter) + + # Mukta seems to be very broken and the height needs to be corrected + h //= 0.7 + + # Draw the letter in the center + draw.text(((size - w) / 2, (size - h) / 2), letter, + font=font, fill=COLOUR_DARK) + + with io.BytesIO() as f: + # If writing out the image does not work with optimization, + # we try to write it out without any optimization. + try: + image.save(f, format, optimize=True, **args) + except: + image.save(f, format, **args) + + return f.getvalue() + + class ListModule(ui_modules.UIModule): def render(self, accounts, show_created_at=False): return self.render_string("users/modules/list.html", accounts=accounts, -- 2.47.3