Start implementing webmention receiving :o the status page is ugly and there's no actual verification yet, but good start at least ;)
This commit is contained in:
parent
b3fb0a8600
commit
169f0687cb
9 changed files with 154 additions and 0 deletions
|
@ -89,6 +89,7 @@ INSTALLED_APPS = [
|
||||||
'lemonauth',
|
'lemonauth',
|
||||||
'micropub',
|
'micropub',
|
||||||
'users',
|
'users',
|
||||||
|
'webmention',
|
||||||
'wellknowns',
|
'wellknowns',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ urlpatterns = [
|
||||||
url('^auth/', include('lemonauth.urls')),
|
url('^auth/', include('lemonauth.urls')),
|
||||||
url('^micropub', include('micropub.urls')),
|
url('^micropub', include('micropub.urls')),
|
||||||
url('^s/', include('shorturls.urls')),
|
url('^s/', include('shorturls.urls')),
|
||||||
|
url('^webmention', include('webmention.urls')),
|
||||||
|
|
||||||
url(r'^django-rq/', include('django_rq.urls')),
|
url(r'^django-rq/', include('django_rq.urls')),
|
||||||
url(r'^sitemap\.xml$', sitemap.index, maps, name='sitemap'),
|
url(r'^sitemap\.xml$', sitemap.index, maps, name='sitemap'),
|
||||||
|
|
1
webmention/__init__.py
Normal file
1
webmention/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'webmention.apps.WebmentionConfig'
|
5
webmention/apps.py
Normal file
5
webmention/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WebmentionConfig(AppConfig):
|
||||||
|
name = 'webmention'
|
39
webmention/migrations/0001_initial.py
Normal file
39
webmention/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.10 on 2018-03-19 01:18
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('entries', '0011_auto_20171120_1108'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Webmention',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('source', models.CharField(max_length=255)),
|
||||||
|
('target', models.CharField(max_length=255)),
|
||||||
|
('state', models.CharField(choices=[('p', 'pending'), ('v', 'valid'), ('i', 'invalid'), ('d', 'deleted')], default='p', max_length=1)),
|
||||||
|
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentions', to='entries.Entry')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'default_related_name': 'mentions',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='webmention',
|
||||||
|
unique_together=set([('source', 'target')]),
|
||||||
|
),
|
||||||
|
]
|
0
webmention/migrations/__init__.py
Normal file
0
webmention/migrations/__init__.py
Normal file
31
webmention/models.py
Normal file
31
webmention/models.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
from django.db import models
|
||||||
|
from entries.models import Entry
|
||||||
|
from model_utils.models import TimeStampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class State:
|
||||||
|
PENDING = 'p'
|
||||||
|
VALID = 'v'
|
||||||
|
INVALID = 'i'
|
||||||
|
DELETED = 'd'
|
||||||
|
CHOICES = (
|
||||||
|
(PENDING, 'pending'),
|
||||||
|
(VALID, 'valid'),
|
||||||
|
(INVALID, 'invalid'),
|
||||||
|
(DELETED, 'deleted'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Webmention(TimeStampedModel):
|
||||||
|
entry = models.ForeignKey(Entry, on_delete=models.CASCADE)
|
||||||
|
source = models.CharField(max_length=255)
|
||||||
|
target = models.CharField(max_length=255)
|
||||||
|
state = models.CharField(
|
||||||
|
choices=State.CHOICES,
|
||||||
|
default=State.PENDING,
|
||||||
|
max_length=1
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_related_name = 'mentions'
|
||||||
|
unique_together = ('source', 'target')
|
8
webmention/urls.py
Normal file
8
webmention/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'webmention'
|
||||||
|
urlpatterns = (
|
||||||
|
url('^$', views.accept, name='accept'),
|
||||||
|
url('^s/(?P<id>\d+)$', views.status, name='status')
|
||||||
|
)
|
68
webmention/views.py
Normal file
68
webmention/views.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.urls import resolve, reverse, Resolver404
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.decorators.http import require_GET, require_POST
|
||||||
|
from entries.models import Entry
|
||||||
|
from lemoncurry.utils import bad_req
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
|
from .models import State, Webmention
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_POST
|
||||||
|
def accept(request):
|
||||||
|
if 'source' not in request.POST:
|
||||||
|
return bad_req('missing source url')
|
||||||
|
source_url = request.POST['source']
|
||||||
|
|
||||||
|
if 'target' not in request.POST:
|
||||||
|
return bad_req('missing target url')
|
||||||
|
target_url = request.POST['target']
|
||||||
|
|
||||||
|
source = urlparse(source_url)
|
||||||
|
target = urlparse(target_url)
|
||||||
|
if source.scheme not in ('http', 'https'):
|
||||||
|
return bad_req('unsupported source scheme')
|
||||||
|
if target.scheme not in ('http', 'https'):
|
||||||
|
return bad_req('unsupported target scheme')
|
||||||
|
if target.netloc != request.site.domain:
|
||||||
|
return bad_req('target not on this site')
|
||||||
|
origin = 'https://' + target.netloc
|
||||||
|
|
||||||
|
try:
|
||||||
|
match = resolve(target.path)
|
||||||
|
except Resolver404:
|
||||||
|
return bad_req('target not found')
|
||||||
|
|
||||||
|
if match.view_name != 'entries:entry':
|
||||||
|
return bad_req('target does not accept webmentions')
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry = Entry.objects.get(pk=match.kwargs['id'])
|
||||||
|
except Entry.DoesNotExist:
|
||||||
|
return bad_req('target not found')
|
||||||
|
|
||||||
|
try:
|
||||||
|
mention = Webmention.objects.get(source=source_url, target=target_url)
|
||||||
|
except Webmention.DoesNotExist:
|
||||||
|
mention = Webmention()
|
||||||
|
mention.source = source_url
|
||||||
|
mention.target = target_url
|
||||||
|
|
||||||
|
mention.entry = entry
|
||||||
|
mention.state = State.PENDING
|
||||||
|
mention.save()
|
||||||
|
status = reverse('webmention:status', kwargs={'id': mention.id})
|
||||||
|
|
||||||
|
res = HttpResponse(status=201)
|
||||||
|
res['Location'] = urljoin(origin, status)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_GET
|
||||||
|
def status(request, id):
|
||||||
|
mention = get_object_or_404(Webmention.objects, pk=id)
|
||||||
|
return HttpResponse(mention.get_state_display())
|
Loading…
Reference in a new issue