mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-18 22:18:02 +00:00
Merge branch 'master' of git.linux-kernel.at:oliver/ivatar into trust
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
image: ofalk/centos7-python36
|
image: docker.io/ofalk/fedora28-python3
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- virtualenv -p python3.6 /tmp/.virtualenv
|
- virtualenv-3 -p python3 /tmp/.virtualenv
|
||||||
- source /tmp/.virtualenv/bin/activate
|
- source /tmp/.virtualenv/bin/activate
|
||||||
- pip install Pillow
|
- pip install Pillow
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -1,5 +1,25 @@
|
|||||||
|
ivatar / libravatar
|
||||||
|
===================
|
||||||
|
|
||||||
|
Pipeline and coverage status
|
||||||
|
============================
|
||||||
|
|
||||||
[](https://git.linux-kernel.at/oliver/ivatar/commits/master)
|
[](https://git.linux-kernel.at/oliver/ivatar/commits/master)
|
||||||
[](http://git.linux-kernel.at/oliver/ivatar/commits/master)
|
[](http://git.linux-kernel.at/oliver/ivatar/commits/master)
|
||||||
|
|
||||||
|
Reports / code documentation
|
||||||
|
============================
|
||||||
|
|
||||||
- [Coverage HTML report](http://oliver.git.linux-kernel.at/ivatar)
|
- [Coverage HTML report](http://oliver.git.linux-kernel.at/ivatar)
|
||||||
- [Code documentation (autogenerated, pycco)](http://oliver.git.linux-kernel.at/ivatar/pycco/)
|
- [Code documentation (autogenerated, pycco)](http://oliver.git.linux-kernel.at/ivatar/pycco/)
|
||||||
|
|
||||||
|
Authors and contributors
|
||||||
|
========================
|
||||||
|
|
||||||
|
Lead developer/Owner: Oliver Falk (aka ofalk or falko) - https://git.linux-kernel.at/oliver
|
||||||
|
Operations: Michal Novotny (aka clime)
|
||||||
|
QA: Tristan Le Guern (aka tleguern)
|
||||||
|
Frontend developer: Niklas Poslovski (aka nipos)
|
||||||
|
Organisation/Meeting moderation: Lars Kruse (aka sumpfralle)
|
||||||
|
|
||||||
|
Initial developer: François Marier - https://fmarier.org/
|
||||||
|
|||||||
@@ -187,3 +187,7 @@ CACHES = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# This is 5 minutes caching for generated/resized images,
|
||||||
|
# so the sites don't hit ivatar so much
|
||||||
|
CACHE_IMAGES_MAX_AGE = 5 * 60
|
||||||
|
|||||||
@@ -28,13 +28,13 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input type="checkbox" name="not_porn" required id="id_not_porn">
|
<input type="checkbox" name="not_porn" required id="id_not_porn">
|
||||||
<label for="id_not_porn">{% trans 'suitable for all ages (i.e. no offensive content)' %}</label>
|
<label for="id_not_porn"><b>{% trans 'required' %}</b>; {% trans 'suitable for all ages (i.e. no offensive content)' %}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input type="checkbox" name="can_distribute" required id="id_can_distribute">
|
<input type="checkbox" name="can_distribute" required id="id_can_distribute">
|
||||||
<label for="id_can_distribute">{% trans 'can be freely copied' %}</label>
|
<label for="id_can_distribute"><b>{% trans 'required' %}</b>; {% trans 'can be freely copied' %}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-default">{% trans 'Upload' %}</button>
|
<button type="submit" class="btn btn-default">{% trans 'Upload' %}</button>
|
||||||
|
|||||||
@@ -816,12 +816,16 @@ class UploadLibravatarExportView(SuccessMessageMixin, FormView):
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
data = self.request.FILES['export_file']
|
data = self.request.FILES['export_file']
|
||||||
items = libravatar_read_gzdata(data.read())
|
try:
|
||||||
# DEBUG print(items)
|
items = libravatar_read_gzdata(data.read())
|
||||||
return render(self.request, 'choose_libravatar_export.html', {
|
# DEBUG print(items)
|
||||||
'emails': items['emails'],
|
return render(self.request, 'choose_libravatar_export.html', {
|
||||||
'photos': items['photos'],
|
'emails': items['emails'],
|
||||||
})
|
'photos': items['photos'],
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, _('Unable to parse file: %s' % e))
|
||||||
|
return HttpResponseRedirect(reverse_lazy('upload_export'))
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name='dispatch')
|
@method_decorator(login_required, name='dispatch')
|
||||||
@@ -914,7 +918,7 @@ class PasswordResetView(PasswordResetViewOriginal):
|
|||||||
try:
|
try:
|
||||||
confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email'])
|
confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email'])
|
||||||
confirmed_email.user.email = confirmed_email.email
|
confirmed_email.user.email = confirmed_email.email
|
||||||
if not confirmed_email.user.password:
|
if not confirmed_email.user.password or confirmed_email.user.password == '!':
|
||||||
random_pass = User.objects.make_random_password()
|
random_pass = User.objects.make_random_password()
|
||||||
confirmed_email.user.set_pasword(random_pass)
|
confirmed_email.user.set_pasword(random_pass)
|
||||||
confirmed_email.user.save()
|
confirmed_email.user.save()
|
||||||
|
|||||||
1
ivatar/static/css/clime.css
Normal file
1
ivatar/static/css/clime.css
Normal file
File diff suppressed because one or more lines are too long
10
ivatar/static/css/clime.less
Normal file
10
ivatar/static/css/clime.less
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import 'tortin.less';
|
||||||
|
@bg-hero:#ff4400;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.content h1, section.content h2, section.content h3, section.content h4, section.content h5, section.content h6 {
|
||||||
|
color: #ff4400;
|
||||||
|
}
|
||||||
@@ -61,5 +61,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
Test incorrect digest
|
Test incorrect digest
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/avatar/%s' % 'x'*65)
|
response = self.client.get('/avatar/%s' % 'x'*65, follow=True)
|
||||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
self.assertRedirects(
|
||||||
|
response=response,
|
||||||
|
expected_url='/static/img/deadbeef.png',
|
||||||
|
msg_prefix='Why does an invalid hash not redirect to deadbeef?')
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if mailurl %}
|
{% if mailurl %}
|
||||||
<div class="panel panel-tortin" style="min-width:132px;width:calc({{ size }}px + 32px);float:left;margin-left:30px">
|
<div class="panel panel-tortin" style="min-width:132px;width:calc({{ size }}px + 33px);float:left;margin-left:20px">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">MD5 <i class="fa fa-lock" title="Secure connection (https)"></i> <i class="fa fa-at" title="mail: {{ form.mail.value }}"></i></h3>
|
<h3 class="panel-title">MD5 <i class="fa fa-lock" title="Secure connection (https)"></i> <i class="fa fa-at" title="mail: {{ form.mail.value }}"></i></h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if openidurl %}
|
{% if openidurl %}
|
||||||
<div class="panel panel-tortin" style="min-width:122px;width:calc({{ size }}px + 33px);float:left;margin-left:30px">
|
<div class="panel panel-tortin" style="min-width:132px;width:calc({{ size }}px + 33px);float:left;margin-left:20px">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">SHA256 <i class="fa fa-lock" title="Secure connection (http)"></i> <i class="fa fa-openid" title="openid: {{ form.openid.value }}"></i></h3>
|
<h3 class="panel-title">SHA256 <i class="fa fa-lock" title="Secure connection (http)"></i> <i class="fa fa-openid" title="openid: {{ form.openid.value }}"></i></h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO TODO TODO: I need better styling -->
|
|
||||||
{% if result %}
|
{% if result %}
|
||||||
<hr/>
|
<hr/>
|
||||||
<h2>The following servers will be used for your domain</h2>
|
<h2>The following servers will be used for your domain</h2>
|
||||||
@@ -44,8 +43,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if result.avatar_server_http %}
|
{% if result.avatar_server_http %}
|
||||||
<a href="{{result.avatar_server_http}}">
|
<a href="http://{{result.avatar_server_http}}">
|
||||||
<h4>{{result.avatar_server_http}}</h4>
|
<h4>http://{{result.avatar_server_http}}</h4>
|
||||||
</a>
|
</a>
|
||||||
{% if result.avatar_server_http_ipv4 %}
|
{% if result.avatar_server_http_ipv4 %}
|
||||||
<br><center>{{ result.avatar_server_http_ipv4 }}</center>
|
<br><center>{{ result.avatar_server_http_ipv4 }}</center>
|
||||||
@@ -66,8 +65,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if result.avatar_server_https %}
|
{% if result.avatar_server_https %}
|
||||||
<a href="{{result.avatar_server_https}}">
|
<a href="https://{{result.avatar_server_https}}">
|
||||||
<h4>{{result.avatar_server_https}}</h4>
|
<h4>https://{{result.avatar_server_https}}</h4>
|
||||||
</a>
|
</a>
|
||||||
{% if result.avatar_server_https_ipv4 %}
|
{% if result.avatar_server_https_ipv4 %}
|
||||||
<br><center>{{ result.avatar_server_https_ipv4 }}</center>
|
<br><center>{{ result.avatar_server_https_ipv4 }}</center>
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ from . views import CheckView, CheckDomainView
|
|||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
url('check/', CheckView.as_view(), name='tools_check'),
|
url('check/', CheckView.as_view(), name='tools_check'),
|
||||||
url('check_domain/', CheckDomainView.as_view(), name='tools_check_domain'),
|
url('check_domain/', CheckDomainView.as_view(), name='tools_check_domain'),
|
||||||
|
url('check_domain$', CheckDomainView.as_view(), name='tools_check_domain'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,13 +21,10 @@ urlpatterns = [ # pylint: disable=invalid-name
|
|||||||
url(
|
url(
|
||||||
r'avatar/(?P<digest>\w{32})',
|
r'avatar/(?P<digest>\w{32})',
|
||||||
AvatarImageView.as_view(), name='avatar_view'),
|
AvatarImageView.as_view(), name='avatar_view'),
|
||||||
|
url(r'avatar/$', AvatarImageView.as_view(), name='avatar_view'),
|
||||||
url(
|
url(
|
||||||
r'avatar/(?P<digest>\w*)',
|
r'avatar/(?P<digest>\w*)',
|
||||||
TemplateView.as_view(
|
RedirectView.as_view(url='/static/img/deadbeef.png'), name='invalid_hash'),
|
||||||
template_name='error.html',
|
|
||||||
extra_context={
|
|
||||||
'errormessage': 'Incorrect digest length',
|
|
||||||
})),
|
|
||||||
url(
|
url(
|
||||||
r'gravatarproxy/(?P<digest>\w*)',
|
r'gravatarproxy/(?P<digest>\w*)',
|
||||||
GravatarProxyView.as_view(), name='gravatarproxy'),
|
GravatarProxyView.as_view(), name='gravatarproxy'),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import pagan
|
|||||||
from robohash import Robohash
|
from robohash import Robohash
|
||||||
|
|
||||||
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
|
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
|
||||||
|
from ivatar.settings import CACHE_IMAGES_MAX_AGE
|
||||||
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
||||||
from . ivataraccount.models import pil_format, file_format
|
from . ivataraccount.models import pil_format, file_format
|
||||||
|
|
||||||
@@ -58,6 +59,11 @@ class AvatarImageView(TemplateView):
|
|||||||
'''
|
'''
|
||||||
# TODO: Do cache resize images!! Memcached?
|
# TODO: Do cache resize images!! Memcached?
|
||||||
|
|
||||||
|
def options(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
||||||
|
response = HttpResponse("", content_type='text/plain')
|
||||||
|
response['Allow'] = "404 mm mp retro pagan wavatar monsterid robohash identicon"
|
||||||
|
return response
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
||||||
'''
|
'''
|
||||||
Override get from parent class
|
Override get from parent class
|
||||||
@@ -142,9 +148,11 @@ class AvatarImageView(TemplateView):
|
|||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
monsterdata.save(data, 'PNG', quality=JPEG_QUALITY)
|
monsterdata.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
return HttpResponse(
|
response = HttpResponse(
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
if str(default) == 'robohash':
|
if str(default) == 'robohash':
|
||||||
roboset = 'any'
|
roboset = 'any'
|
||||||
@@ -155,9 +163,11 @@ class AvatarImageView(TemplateView):
|
|||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
robohash.img.save(data, format='png')
|
robohash.img.save(data, format='png')
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
return HttpResponse(
|
response = HttpResponse(
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
if str(default) == 'retro':
|
if str(default) == 'retro':
|
||||||
identicon = Identicon.render(kwargs['digest'])
|
identicon = Identicon.render(kwargs['digest'])
|
||||||
@@ -166,9 +176,11 @@ class AvatarImageView(TemplateView):
|
|||||||
img = img.resize((size, size), Image.ANTIALIAS)
|
img = img.resize((size, size), Image.ANTIALIAS)
|
||||||
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
return HttpResponse(
|
response = HttpResponse(
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
if str(default) == 'pagan':
|
if str(default) == 'pagan':
|
||||||
paganobj = pagan.Avatar(kwargs['digest'])
|
paganobj = pagan.Avatar(kwargs['digest'])
|
||||||
@@ -176,9 +188,11 @@ class AvatarImageView(TemplateView):
|
|||||||
img = paganobj.img.resize((size, size), Image.ANTIALIAS)
|
img = paganobj.img.resize((size, size), Image.ANTIALIAS)
|
||||||
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
return HttpResponse(
|
response = HttpResponse(
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
if str(default) == 'identicon':
|
if str(default) == 'identicon':
|
||||||
p = Pydenticon5()
|
p = Pydenticon5()
|
||||||
@@ -188,9 +202,11 @@ class AvatarImageView(TemplateView):
|
|||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
return HttpResponse(
|
response = HttpResponse(
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
if str(default) == 'mm' or str(default) == 'mp':
|
if str(default) == 'mm' or str(default) == 'mp':
|
||||||
# If mm is explicitly given, we need to catch that
|
# If mm is explicitly given, we need to catch that
|
||||||
@@ -226,9 +242,11 @@ class AvatarImageView(TemplateView):
|
|||||||
obj.save()
|
obj.save()
|
||||||
if imgformat == 'jpg':
|
if imgformat == 'jpg':
|
||||||
imgformat = 'jpeg'
|
imgformat = 'jpeg'
|
||||||
return HttpResponse(
|
response = HttpResponse(
|
||||||
data,
|
data,
|
||||||
content_type='image/%s' % imgformat)
|
content_type='image/%s' % imgformat)
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
class GravatarProxyView(View):
|
class GravatarProxyView(View):
|
||||||
'''
|
'''
|
||||||
@@ -297,9 +315,11 @@ class GravatarProxyView(View):
|
|||||||
data = BytesIO(gravatarimagedata.read())
|
data = BytesIO(gravatarimagedata.read())
|
||||||
img = Image.open(data)
|
img = Image.open(data)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
return HttpResponse(
|
response = HttpResponse(
|
||||||
data.read(),
|
data.read(),
|
||||||
content_type='image/%s' % file_format(img.format))
|
content_type='image/%s' % file_format(img.format))
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
print('Value error: %s' % exc)
|
print('Value error: %s' % exc)
|
||||||
|
|||||||
22
libravatarproxy.py
Executable file
22
libravatarproxy.py
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.stderr.buffer.write(b'%s' % bytes(os.environ.get("QUERY_STRING", "No Query String in url"), 'utf-8'))
|
||||||
|
|
||||||
|
link = 'https://www.libravatar.org/avatar/%s' % os.environ.get("QUERY_STRING", 'x'*32)
|
||||||
|
sys.stderr.buffer.write(b'%s' % bytes(link, 'utf-8'))
|
||||||
|
|
||||||
|
data = None
|
||||||
|
with urllib.request.urlopen(link) as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
for header in f.headers._headers:
|
||||||
|
if header[0] == 'Content-Type':
|
||||||
|
sys.stdout.buffer.write(b"%s: %s\n\n" % (bytes(header[0], 'utf-8'), bytes(header[1], 'utf-8')))
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
@@ -30,7 +30,7 @@ wheel
|
|||||||
yapf
|
yapf
|
||||||
django-anymail[mailgun]
|
django-anymail[mailgun]
|
||||||
mysqlclient
|
mysqlclient
|
||||||
psycopg2
|
psycopg2-binary
|
||||||
notsetuptools
|
notsetuptools
|
||||||
git+https://github.com/ofalk/monsterid.git
|
git+https://github.com/ofalk/monsterid.git
|
||||||
git+https://github.com/ofalk/Robohash.git@devel
|
git+https://github.com/ofalk/Robohash.git@devel
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ If you've got a proposal to discuss or prefer to write to us, you can join our <
|
|||||||
|
|
||||||
You can also put short notices to our attention on <a href="http://identi.ca/libravatar" title="http://identi.ca/libravatar">Identica<a/> or <a href="http://twitter.com/libravatar" title="http://twitter.com/libravatar">Twitter</a>.
|
You can also put short notices to our attention on <a href="http://identi.ca/libravatar" title="http://identi.ca/libravatar">Identica<a/> or <a href="http://twitter.com/libravatar" title="http://twitter.com/libravatar">Twitter</a>.
|
||||||
|
|
||||||
|
<h4>Mastodon</h4>
|
||||||
|
|
||||||
|
Our Mastodon profile is available on <a href="https://photog.social/@libravatar">https://photog.social/@libravatar</a>.
|
||||||
|
|
||||||
<h4>Email</h4>
|
<h4>Email</h4>
|
||||||
|
|
||||||
Finally, if you need to email us: <a href="mailto:dev@libravatar.org" title="mailto:dev@libravatar.org">dev@libravatar.org</a>
|
Finally, if you need to email us: <a href="mailto:dev@libravatar.org" title="mailto:dev@libravatar.org">dev@libravatar.org</a>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="msapplication-TileImage" content="{% static '/img/nobody/144.png' %}">
|
<meta name="msapplication-TileImage" content="{% static '/img/nobody/144.png' %}">
|
||||||
<meta name="msapplication-TileColor" content="#36b7d7">
|
<meta name="msapplication-TileColor" content="#36b7d7">
|
||||||
<meta name="apple-mobile-web-app-title" content="ivatar">
|
<meta name="apple-mobile-web-app-title" content="ivatar">
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/tools/check/" class="btn btn-lg btn-primary">{% trans 'Check email' %}</a>
|
<a href="/tools/check/" class="btn btn-lg btn-primary">{% trans 'Check' %}</a>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
<a class="btn btn-default" href="https://wiki.libravatar.org/">{% trans 'Wiki' %}</a><br/>
|
<a class="btn btn-default" href="https://wiki.libravatar.org/">{% trans 'Wiki' %}</a><br/>
|
||||||
<a class="btn btn-default" href="http://blog.libravatar.org/">{% trans 'Blog' %}</a><br/>
|
<a class="btn btn-default" href="http://blog.libravatar.org/">{% trans 'Blog' %}</a><br/>
|
||||||
<h3>{% trans 'Social media' %}</h3>
|
<h3>{% trans 'Social media' %}</h3>
|
||||||
|
<a class="btn btn-default" rel="me" href="https://photog.social/@libravatar">Mastodon</a><br/>
|
||||||
<a class="btn btn-default" href="https://identi.ca/libravatar">Identica</a><br/>
|
<a class="btn btn-default" href="https://identi.ca/libravatar">Identica</a><br/>
|
||||||
<a class="btn btn-default" href="https://twitter.com/libravatar">Twitter</a><br/>
|
<a class="btn btn-default" href="https://twitter.com/libravatar">Twitter</a><br/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user