From 43348a89da4ff07b4acc8663bc1de69630fe689b Mon Sep 17 00:00:00 2001 From: Danielle McLean Date: Fri, 23 Mar 2018 12:56:13 +1100 Subject: [PATCH] Add support for serving users' avatars through the Libravatar API --- Pipfile | 1 + Pipfile.lock | 88 ++++++++++++--------- lemoncurry/settings/base.py | 1 + lemoncurry/urls.py | 1 + users/migrations/0013_auto_20180323_1200.py | 24 ++++++ users/models.py | 17 ++++ users/urls.py | 8 ++ users/views.py | 49 ++++++++++++ 8 files changed, 152 insertions(+), 37 deletions(-) create mode 100644 users/migrations/0013_auto_20180323_1200.py create mode 100644 users/urls.py create mode 100644 users/views.py diff --git a/Pipfile b/Pipfile index cf23c65..d12f0ac 100644 --- a/Pipfile +++ b/Pipfile @@ -47,6 +47,7 @@ django-cors-headers = "*" pytest-django = "*" "argon2-cffi" = "*" python-baseconv = "*" +django-computed-property = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 8d7df8e..9010828 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a824f0af4df1d0088d5dd66db92329358a5cf0095c71f1cb59f6e60583b912f5" + "sha256": "0b68c7a2821e8063bc9d7bae759a9d8dd57785e341e6d23d6421ad93310c7d1b" }, "host-environment-markers": { "implementation_name": "cpython", @@ -11,7 +11,7 @@ "platform_python_implementation": "CPython", "platform_release": "17.5.0", "platform_system": "Darwin", - "platform_version": "Darwin Kernel Version 17.5.0: Mon Mar 5 22:58:50 PST 2018; root:xnu-4570.51.1~2/RELEASE_X86_64", + "platform_version": "Darwin Kernel Version 17.5.0: Tue Mar 13 20:39:15 PDT 2018; root:xnu-4570.51.1~36/RELEASE_X86_64", "python_full_version": "3.6.4", "python_version": "3.6", "sys_platform": "darwin" @@ -206,6 +206,12 @@ ], "version": "==2.2" }, + "django-computed-property": { + "hashes": [ + "sha256:31aa6453a5c504ce196ba9ae3bacbe0557cadf7ae89e25431b90bf206febd3b3" + ], + "version": "==0.2.1" + }, "django-cors-headers": { "hashes": [ "sha256:0e9532628b3aa8806442d4d0b15e56112e6cfbef3735e13401935c98b842a2b4", @@ -323,36 +329,36 @@ }, "lxml": { "hashes": [ - "sha256:0af9c9267b1257319d49e9c1e9abbf92a99f965bee3c4733e0f0f7578985182d", - "sha256:62bfcd0629991e1c1257ffd28df2ab31a5c44da4c06823c26ec0f472723a84ca", - "sha256:1d1e45584353e4d563685874707fc8c85cdd11b0ef3b79d77bb38046134d68a9", - "sha256:29697224b2df76edf7c2de9bcd90a26dd28fe85c5fd7f0171cae84f8383b227e", - "sha256:988d55112f196e12341b7c5138841c2b4f21f871eaa8f138c6ac4c46f28899f9", - "sha256:57be98177ce784495dff53f40620995ad0a56456246ed9d51977e595de58e12e", - "sha256:36ffb216e2f361a5a0a7e219aea6cd44da11c64061baed273944aae21223186c", - "sha256:d0dc3e5737adcc9a23fd3d3d3072b887fefb48143309563f412ef7b0ebdfdb30", - "sha256:7769ac9203ebe6d8db16904c54d57d77360fcc1926ed7afaa86b04050e4afa5b", - "sha256:4c21d7304d37715e6aed756e4d0c374c99c9bb1fa8d64f546b95474b17ac23de", - "sha256:0aa44ffdeaaf6ba45d61980bb2c07e87d4dcac7a8b5b9d458124bc1adcda5233", - "sha256:71ac6dac6835de75aaf531cae9ffa447dae0783ba1f43bf6eaccfad3680a5b9c", - "sha256:88583c6565c9299f617238a500f1a47510bac54daff7872d6a343f13361b659e", - "sha256:2812bc45a7f53f366217b76a1c53e6728fbfa7f7524d16a321ea8f7131428bd1", - "sha256:124a9d529eec5e10f307eb237df3efc43dd1fb7ebdb5da5e480c4ed372648b6b", - "sha256:f04b184984c23e0caac3c55eac2fe2dbb88726a5a1b35e23715eff6f29a4705c", - "sha256:d06260e6102b2f18dbee3736185cd6a2e1c88c0fad782bf8e9d7a7a1b24e02b0", - "sha256:0cddc6cde79e1932efc71d9974a4418184ad0b8ca46c633ad772b2c5eaf36b3c", - "sha256:9e08918b744b89d30750eca8598f37ae75b16202870db678fde970d85afed3e3", - "sha256:b46f31e806f6884bd1053ad1d78ecaca6d1bc5dd94a1b783a6ff0bb4b3a60962", - "sha256:dd98d4f88ce0abda2b02c1542d1de22dd342023f3ba09874bd95841283f29433", - "sha256:8f52c4c8f1cf15419193026e731f34a3260a3ce7977b875ba1eb2517b8a3f660", - "sha256:c18f316cad969111b1ff9e84c82fbc9ae6f25f35701118182d384585940cdf80", - "sha256:95b82fdfdaac71640b281da6b9a2c3700177ba5190a786881b184de744ad55de", - "sha256:cef79715f2335bfc1ef7082bcb8b2bac87271431653455221a9127fde146208c", - "sha256:4626d699551f66687e5f7e7f9b79bfce611e12edebfb9fec276e2df8ec46541e", - "sha256:cf63f590090404c52f179b7ceacb7cd549de3a1697bcfe2f79be180b2801d109", - "sha256:7d96fbb5f23a62300aa9bef7d286cd61aca8902357619c8708c0290aba5df73f" + "sha256:65a272821d5d8194358d6b46f3ca727fa56a6b63981606eac737c86d27309cdd", + "sha256:abbd2fb4a5a04c11b5e04eb146659a0cf67bb237dd3d7ca3b9994d3a9f826e55", + "sha256:3682a17fbf72d56d7e46db2e80ca23850b79c28cfe75dcd9b82f58808f730909", + "sha256:75322a531504d4f383264391d89993a42e286da8821ddc5ac315e57305cb84f0", + "sha256:01c45df6d90497c20aa2a07789a41941f9a1029faa30bf725fc7f6d515b1afe9", + "sha256:34d49d0f72dd82b9530322c48b70ac78cca0911275da741c3b1d2f3603c5f295", + "sha256:1b46f37927fa6cd1f3fe34b54f1a23bd5bea1d905657289e08e1297069a1a597", + "sha256:5b653c9379ce29ce271fbe1010c5396670f018e78b643e21beefbb3dc6d291de", + "sha256:3cf2830b9a6ad7f6e965fa53a768d4d2372a7856f20ffa6ce43d2fe9c0d34b19", + "sha256:1b164bba1320b14905dcff77da10d5ce9c411ac4acc4fb4ed9a2a4d10fae38c9", + "sha256:c557ad647facb3c0027a9d0af58853f905e85a0a2f04dcb73f8e665272fcdc3a", + "sha256:6b6379495d3baacf7ed755ac68547c8dff6ce5d37bf370f0b7678888dc1283f9", + "sha256:8523fbde9c2216f3f2b950cb01ebe52e785eaa8a07ffeb456dd3576ca1b4fb9b", + "sha256:a7182ea298cc3555ea56ffbb0748fe0d5e0d81451e2bc16d7f4645cd01b1ca70", + "sha256:7f457cbda964257f443bac861d3a36732dcba8183149e7818ee2fb7c86901b94", + "sha256:0c9fef4f8d444e337df96c54544aeb85b7215b2ed7483bb6c35de97ac99f1bcd", + "sha256:7ff1fc76d8804e0f870c343a72007ff587090c218b0f92d8ee784ac2b6eaf5b9", + "sha256:accc9f6b77bed0a6f267b4fae120f6008a951193d548cdbe9b61fc98a08b1cf8", + "sha256:bd88c8ce0d1504fdfd96a35911dd4f3edfb2e560d7cfdb5a3d09aa571ae5fbae", + "sha256:defabb7fbb99f9f7b3e0b24b286a46855caef4776495211b066e9e6592d12b04", + "sha256:231047b05907315ae9a9b6925751f9fd2c479cf7b100fff62485a25e382ca0d4", + "sha256:0e7996e9b46b4d8b4ac1c329a00e2d10edcd8380b95d2a676fccabf4c1dd0512", + "sha256:691f2cd97cf026c611df1ea5055755eec7f878f2d4f4330dc8686583de6fc5fd", + "sha256:1858b1933d483ec5727549d3fe166eeb54229fbd6a9d3d7ea26d2c8a28048058", + "sha256:0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b", + "sha256:28f0c6652c1b130f1e576b60532f84b19379485eb8da6185c29bd8c9c9bc97bf", + "sha256:8f37627f16e026523fca326f1b5c9a43534862fede6c3e99c2ba6a776d75c1ab", + "sha256:e2629cdbcad82b83922a3488937632a4983ecc0fed3e5cfbf430d069382eeb9b" ], - "version": "==4.2.0" + "version": "==4.2.1" }, "markdown": { "hashes": [ @@ -373,6 +379,14 @@ ], "version": "==0.5.0" }, + "more-itertools": { + "hashes": [ + "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e", + "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea", + "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44" + ], + "version": "==4.1.0" + }, "msgpack-python": { "hashes": [ "sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b" @@ -465,10 +479,10 @@ }, "py": { "hashes": [ - "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", - "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" + "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a", + "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881" ], - "version": "==1.5.2" + "version": "==1.5.3" }, "pycparser": { "hashes": [ @@ -506,10 +520,10 @@ }, "pytest": { "hashes": [ - "sha256:062027955bccbc04d2fcd5d79690947e018ba31abe4c90b2c6721abec734261b", - "sha256:117bad36c1a787e1a8a659df35de53ba05f9f3398fb9e4ac17e80ad5903eb8c5" + "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", + "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1" ], - "version": "==3.4.2" + "version": "==3.5.0" }, "pytest-django": { "hashes": [ diff --git a/lemoncurry/settings/base.py b/lemoncurry/settings/base.py index c1ed6b5..9efb72c 100644 --- a/lemoncurry/settings/base.py +++ b/lemoncurry/settings/base.py @@ -72,6 +72,7 @@ INSTALLED_APPS = [ 'analytical', 'annoying', 'compressor', + 'computed_property', 'corsheaders', 'debug_toolbar', 'django_activeurl', diff --git a/lemoncurry/urls.py b/lemoncurry/urls.py index 302ce4a..da2237e 100644 --- a/lemoncurry/urls.py +++ b/lemoncurry/urls.py @@ -36,6 +36,7 @@ maps = {'sitemaps': sections} urlpatterns = [ url('', include('home.urls')), url('', include('entries.urls')), + url('', include('users.urls')), url('^.well-known/', include('wellknowns.urls')), url('^admin/', otp_admin_site.urls), url('^auth/', include('lemonauth.urls')), diff --git a/users/migrations/0013_auto_20180323_1200.py b/users/migrations/0013_auto_20180323_1200.py new file mode 100644 index 0000000..953b271 --- /dev/null +++ b/users/migrations/0013_auto_20180323_1200.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.3 on 2018-03-23 01:00 + +import computed_property.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0012_auto_20180129_1614'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='email_md5', + field=computed_property.fields.ComputedCharField(compute_from='calc_email_md5', default='', editable=False, max_length=32, unique=True), + ), + migrations.AddField( + model_name='user', + name='email_sha256', + field=computed_property.fields.ComputedCharField(compute_from='calc_email_sha256', default='', editable=False, max_length=64, unique=True), + ), + ] diff --git a/users/models.py b/users/models.py index cf42a50..8fd1b59 100644 --- a/users/models.py +++ b/users/models.py @@ -1,7 +1,9 @@ +from computed_property import ComputedCharField from django.db import models from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager from django.contrib.sites.models import Site as DjangoSite from django.utils.functional import cached_property +from hashlib import md5, sha256 from meta.models import ModelMeta from urllib.parse import urljoin from lemoncurry import utils @@ -46,6 +48,21 @@ class User(ModelMeta, AbstractUser): # This is gonna need to change if I ever decide to add multiple-user support ;) url = '/' + email_md5 = ComputedCharField( + compute_from='calc_email_md5', max_length=32, unique=True + ) + email_sha256 = ComputedCharField( + compute_from='calc_email_sha256', max_length=64, unique=True + ) + + @property + def calc_email_md5(self): + return md5(self.email.lower().encode('utf-8')).hexdigest() + + @property + def calc_email_sha256(self): + return sha256(self.email.lower().encode('utf-8')).hexdigest() + @property def name(self): return '{0} {1}'.format(self.first_name, self.last_name) diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..141b45e --- /dev/null +++ b/users/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url + +from .views import libravatar + +app_name = 'users' +urlpatterns = ( + url('^avatar/(?P[a-z0-9]+)$', libravatar, name='libravatar'), +) diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..ca9bc22 --- /dev/null +++ b/users/views.py @@ -0,0 +1,49 @@ +from django.http import HttpResponse, HttpResponseRedirect +from PIL import Image + +from lemoncurry import utils +from .models import User + + +def try_libravatar_org(hash, get): + url = 'https://seccdn.libravatar.org/avatar/' + hash + if get: + url += '?' + get.urlencode() + return HttpResponseRedirect(url) + + +def libravatar(request, hash): + g = request.GET + size = g.get('s', g.get('size', 80)) + try: + size = int(size) + except ValueError: + return utils.bad_req('size parameter must be an integer') + if not 1 <= size <= 128: + return utils.bad_req('size parameter must be between 1 and 128') + + if len(hash) == 32: + where = {'email_md5': hash} + elif len(hash) == 64: + where = {'email_sha256': hash} + else: + return utils.bad_req('hash must be either md5 or sha256') + + # If the user doesn't exist or lacks an avatar, see if libravatar.org has + # one for them - libravatar.org falls back to Gravatar when possible (only + # for MD5 hashes, since Gravatar doesn't support SHA-256), so this ensures + # all the most likely places are checked. + try: + user = User.objects.get(**where) + except User.DoesNotExist: + return try_libravatar_org(hash, g) + + if not user.avatar: + return try_libravatar_org(hash, g) + + im = Image.open(user.avatar) + im_resized = im.resize((size, size)) + + response = HttpResponse(content_type='image/'+im.format.lower()) + im_resized.save(response, im.format) + return response