Add support for serving users' avatars through the Libravatar API

This commit is contained in:
Danielle McLean 2018-03-23 12:56:13 +11:00
parent 6fb289727c
commit 43348a89da
Signed by: 00dani
GPG key ID: 5A5D2D1AFF12EEC5
8 changed files with 152 additions and 37 deletions

View file

@ -47,6 +47,7 @@ django-cors-headers = "*"
pytest-django = "*" pytest-django = "*"
"argon2-cffi" = "*" "argon2-cffi" = "*"
python-baseconv = "*" python-baseconv = "*"
django-computed-property = "*"
[dev-packages] [dev-packages]

88
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "a824f0af4df1d0088d5dd66db92329358a5cf0095c71f1cb59f6e60583b912f5" "sha256": "0b68c7a2821e8063bc9d7bae759a9d8dd57785e341e6d23d6421ad93310c7d1b"
}, },
"host-environment-markers": { "host-environment-markers": {
"implementation_name": "cpython", "implementation_name": "cpython",
@ -11,7 +11,7 @@
"platform_python_implementation": "CPython", "platform_python_implementation": "CPython",
"platform_release": "17.5.0", "platform_release": "17.5.0",
"platform_system": "Darwin", "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_full_version": "3.6.4",
"python_version": "3.6", "python_version": "3.6",
"sys_platform": "darwin" "sys_platform": "darwin"
@ -206,6 +206,12 @@
], ],
"version": "==2.2" "version": "==2.2"
}, },
"django-computed-property": {
"hashes": [
"sha256:31aa6453a5c504ce196ba9ae3bacbe0557cadf7ae89e25431b90bf206febd3b3"
],
"version": "==0.2.1"
},
"django-cors-headers": { "django-cors-headers": {
"hashes": [ "hashes": [
"sha256:0e9532628b3aa8806442d4d0b15e56112e6cfbef3735e13401935c98b842a2b4", "sha256:0e9532628b3aa8806442d4d0b15e56112e6cfbef3735e13401935c98b842a2b4",
@ -323,36 +329,36 @@
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
"sha256:0af9c9267b1257319d49e9c1e9abbf92a99f965bee3c4733e0f0f7578985182d", "sha256:65a272821d5d8194358d6b46f3ca727fa56a6b63981606eac737c86d27309cdd",
"sha256:62bfcd0629991e1c1257ffd28df2ab31a5c44da4c06823c26ec0f472723a84ca", "sha256:abbd2fb4a5a04c11b5e04eb146659a0cf67bb237dd3d7ca3b9994d3a9f826e55",
"sha256:1d1e45584353e4d563685874707fc8c85cdd11b0ef3b79d77bb38046134d68a9", "sha256:3682a17fbf72d56d7e46db2e80ca23850b79c28cfe75dcd9b82f58808f730909",
"sha256:29697224b2df76edf7c2de9bcd90a26dd28fe85c5fd7f0171cae84f8383b227e", "sha256:75322a531504d4f383264391d89993a42e286da8821ddc5ac315e57305cb84f0",
"sha256:988d55112f196e12341b7c5138841c2b4f21f871eaa8f138c6ac4c46f28899f9", "sha256:01c45df6d90497c20aa2a07789a41941f9a1029faa30bf725fc7f6d515b1afe9",
"sha256:57be98177ce784495dff53f40620995ad0a56456246ed9d51977e595de58e12e", "sha256:34d49d0f72dd82b9530322c48b70ac78cca0911275da741c3b1d2f3603c5f295",
"sha256:36ffb216e2f361a5a0a7e219aea6cd44da11c64061baed273944aae21223186c", "sha256:1b46f37927fa6cd1f3fe34b54f1a23bd5bea1d905657289e08e1297069a1a597",
"sha256:d0dc3e5737adcc9a23fd3d3d3072b887fefb48143309563f412ef7b0ebdfdb30", "sha256:5b653c9379ce29ce271fbe1010c5396670f018e78b643e21beefbb3dc6d291de",
"sha256:7769ac9203ebe6d8db16904c54d57d77360fcc1926ed7afaa86b04050e4afa5b", "sha256:3cf2830b9a6ad7f6e965fa53a768d4d2372a7856f20ffa6ce43d2fe9c0d34b19",
"sha256:4c21d7304d37715e6aed756e4d0c374c99c9bb1fa8d64f546b95474b17ac23de", "sha256:1b164bba1320b14905dcff77da10d5ce9c411ac4acc4fb4ed9a2a4d10fae38c9",
"sha256:0aa44ffdeaaf6ba45d61980bb2c07e87d4dcac7a8b5b9d458124bc1adcda5233", "sha256:c557ad647facb3c0027a9d0af58853f905e85a0a2f04dcb73f8e665272fcdc3a",
"sha256:71ac6dac6835de75aaf531cae9ffa447dae0783ba1f43bf6eaccfad3680a5b9c", "sha256:6b6379495d3baacf7ed755ac68547c8dff6ce5d37bf370f0b7678888dc1283f9",
"sha256:88583c6565c9299f617238a500f1a47510bac54daff7872d6a343f13361b659e", "sha256:8523fbde9c2216f3f2b950cb01ebe52e785eaa8a07ffeb456dd3576ca1b4fb9b",
"sha256:2812bc45a7f53f366217b76a1c53e6728fbfa7f7524d16a321ea8f7131428bd1", "sha256:a7182ea298cc3555ea56ffbb0748fe0d5e0d81451e2bc16d7f4645cd01b1ca70",
"sha256:124a9d529eec5e10f307eb237df3efc43dd1fb7ebdb5da5e480c4ed372648b6b", "sha256:7f457cbda964257f443bac861d3a36732dcba8183149e7818ee2fb7c86901b94",
"sha256:f04b184984c23e0caac3c55eac2fe2dbb88726a5a1b35e23715eff6f29a4705c", "sha256:0c9fef4f8d444e337df96c54544aeb85b7215b2ed7483bb6c35de97ac99f1bcd",
"sha256:d06260e6102b2f18dbee3736185cd6a2e1c88c0fad782bf8e9d7a7a1b24e02b0", "sha256:7ff1fc76d8804e0f870c343a72007ff587090c218b0f92d8ee784ac2b6eaf5b9",
"sha256:0cddc6cde79e1932efc71d9974a4418184ad0b8ca46c633ad772b2c5eaf36b3c", "sha256:accc9f6b77bed0a6f267b4fae120f6008a951193d548cdbe9b61fc98a08b1cf8",
"sha256:9e08918b744b89d30750eca8598f37ae75b16202870db678fde970d85afed3e3", "sha256:bd88c8ce0d1504fdfd96a35911dd4f3edfb2e560d7cfdb5a3d09aa571ae5fbae",
"sha256:b46f31e806f6884bd1053ad1d78ecaca6d1bc5dd94a1b783a6ff0bb4b3a60962", "sha256:defabb7fbb99f9f7b3e0b24b286a46855caef4776495211b066e9e6592d12b04",
"sha256:dd98d4f88ce0abda2b02c1542d1de22dd342023f3ba09874bd95841283f29433", "sha256:231047b05907315ae9a9b6925751f9fd2c479cf7b100fff62485a25e382ca0d4",
"sha256:8f52c4c8f1cf15419193026e731f34a3260a3ce7977b875ba1eb2517b8a3f660", "sha256:0e7996e9b46b4d8b4ac1c329a00e2d10edcd8380b95d2a676fccabf4c1dd0512",
"sha256:c18f316cad969111b1ff9e84c82fbc9ae6f25f35701118182d384585940cdf80", "sha256:691f2cd97cf026c611df1ea5055755eec7f878f2d4f4330dc8686583de6fc5fd",
"sha256:95b82fdfdaac71640b281da6b9a2c3700177ba5190a786881b184de744ad55de", "sha256:1858b1933d483ec5727549d3fe166eeb54229fbd6a9d3d7ea26d2c8a28048058",
"sha256:cef79715f2335bfc1ef7082bcb8b2bac87271431653455221a9127fde146208c", "sha256:0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b",
"sha256:4626d699551f66687e5f7e7f9b79bfce611e12edebfb9fec276e2df8ec46541e", "sha256:28f0c6652c1b130f1e576b60532f84b19379485eb8da6185c29bd8c9c9bc97bf",
"sha256:cf63f590090404c52f179b7ceacb7cd549de3a1697bcfe2f79be180b2801d109", "sha256:8f37627f16e026523fca326f1b5c9a43534862fede6c3e99c2ba6a776d75c1ab",
"sha256:7d96fbb5f23a62300aa9bef7d286cd61aca8902357619c8708c0290aba5df73f" "sha256:e2629cdbcad82b83922a3488937632a4983ecc0fed3e5cfbf430d069382eeb9b"
], ],
"version": "==4.2.0" "version": "==4.2.1"
}, },
"markdown": { "markdown": {
"hashes": [ "hashes": [
@ -373,6 +379,14 @@
], ],
"version": "==0.5.0" "version": "==0.5.0"
}, },
"more-itertools": {
"hashes": [
"sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e",
"sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea",
"sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"
],
"version": "==4.1.0"
},
"msgpack-python": { "msgpack-python": {
"hashes": [ "hashes": [
"sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b" "sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b"
@ -465,10 +479,10 @@
}, },
"py": { "py": {
"hashes": [ "hashes": [
"sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a",
"sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881"
], ],
"version": "==1.5.2" "version": "==1.5.3"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
@ -506,10 +520,10 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:062027955bccbc04d2fcd5d79690947e018ba31abe4c90b2c6721abec734261b", "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c",
"sha256:117bad36c1a787e1a8a659df35de53ba05f9f3398fb9e4ac17e80ad5903eb8c5" "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"
], ],
"version": "==3.4.2" "version": "==3.5.0"
}, },
"pytest-django": { "pytest-django": {
"hashes": [ "hashes": [

View file

@ -72,6 +72,7 @@ INSTALLED_APPS = [
'analytical', 'analytical',
'annoying', 'annoying',
'compressor', 'compressor',
'computed_property',
'corsheaders', 'corsheaders',
'debug_toolbar', 'debug_toolbar',
'django_activeurl', 'django_activeurl',

View file

@ -36,6 +36,7 @@ maps = {'sitemaps': sections}
urlpatterns = [ urlpatterns = [
url('', include('home.urls')), url('', include('home.urls')),
url('', include('entries.urls')), url('', include('entries.urls')),
url('', include('users.urls')),
url('^.well-known/', include('wellknowns.urls')), url('^.well-known/', include('wellknowns.urls')),
url('^admin/', otp_admin_site.urls), url('^admin/', otp_admin_site.urls),
url('^auth/', include('lemonauth.urls')), url('^auth/', include('lemonauth.urls')),

View file

@ -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),
),
]

View file

@ -1,7 +1,9 @@
from computed_property import ComputedCharField
from django.db import models from django.db import models
from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager
from django.contrib.sites.models import Site as DjangoSite from django.contrib.sites.models import Site as DjangoSite
from django.utils.functional import cached_property from django.utils.functional import cached_property
from hashlib import md5, sha256
from meta.models import ModelMeta from meta.models import ModelMeta
from urllib.parse import urljoin from urllib.parse import urljoin
from lemoncurry import utils 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 ;) # This is gonna need to change if I ever decide to add multiple-user support ;)
url = '/' 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 @property
def name(self): def name(self):
return '{0} {1}'.format(self.first_name, self.last_name) return '{0} {1}'.format(self.first_name, self.last_name)

8
users/urls.py Normal file
View file

@ -0,0 +1,8 @@
from django.conf.urls import url
from .views import libravatar
app_name = 'users'
urlpatterns = (
url('^avatar/(?P<hash>[a-z0-9]+)$', libravatar, name='libravatar'),
)

49
users/views.py Normal file
View file

@ -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