diff --git a/fedireads/api.py b/fedireads/api.py new file mode 100644 index 000000000..bf2624bec --- /dev/null +++ b/fedireads/api.py @@ -0,0 +1,15 @@ +def get_or_create_remote_user(actor): + ''' wow, a foreigner ''' + try: + user = models.User.objects.get(actor=actor) + except models.User.DoesNotExist: + # TODO: how do you actually correctly learn this? + username = '%s@%s' % (actor.split('/')[-1], actor.split('/')[2]) + user = models.User.objects.create_user( + username, + '', '', + actor=actor, + local=False + ) + return user + diff --git a/fedireads/federation.py b/fedireads/federation.py deleted file mode 100644 index 41c360402..000000000 --- a/fedireads/federation.py +++ /dev/null @@ -1,415 +0,0 @@ -''' activitystream api ''' -from base64 import b64encode -from Crypto.PublicKey import RSA -from Crypto.Signature import pkcs1_15 -from Crypto.Hash import SHA256 -from datetime import datetime -from django.http import HttpResponse, HttpResponseBadRequest, \ - HttpResponseNotFound, JsonResponse -from django.views.decorators.csrf import csrf_exempt -from fedireads.settings import DOMAIN -from fedireads.openlibrary import get_or_create_book -from fedireads import models -import json -import requests -import re -from uuid import uuid4 - -def webfinger(request): - ''' allow other servers to ask about a user ''' - resource = request.GET.get('resource') - if not resource and not resource.startswith('acct:'): - return HttpResponseBadRequest() - ap_id = resource.replace('acct:', '') - user = models.User.objects.filter(username=ap_id).first() - if not user: - return HttpResponseNotFound('No account found') - return JsonResponse({ - 'subject': 'acct:%s' % (user.username), - 'links': [ - { - 'rel': 'self', - 'type': 'application/activity+json', - 'href': user.actor - } - ] - }) - - -@csrf_exempt -def get_actor(request, username): - ''' return an activitypub actor object ''' - if request.method != 'GET': - return HttpResponseBadRequest() - - user = models.User.objects.get(localname=username) - return JsonResponse({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1' - ], - - 'id': user.actor, - 'type': 'Person', - 'preferredUsername': user.localname, - 'inbox': format_inbox(user), - 'followers': '%s/followers' % user.actor, - 'following': '%s/following' % user.actor, - 'summary': user.summary, - 'publicKey': { - 'id': '%s/#main-key' % user.actor, - 'owner': user.actor, - 'publicKeyPem': user.public_key, - }, - 'endpoints': { - 'sharedInbox': 'https://%s/inbox' % DOMAIN, - } - }) - - -@csrf_exempt -def inbox(request, username): - ''' incoming activitypub events ''' - if request.method == 'GET': - # TODO: return a collection of something? - return JsonResponse({}) - - # TODO: RSA key verification - - try: - activity = json.loads(request.body) - except json.decoder.JSONDecodeError: - return HttpResponseBadRequest - - # TODO: should do some kind of checking if the user accepts - # this action from the sender - # but this will just throw an error if the user doesn't exist I guess - models.User.objects.get(localname=username) - - if activity['type'] == 'Add': - return handle_incoming_shelve(activity) - - if activity['type'] == 'Follow': - return handle_incoming_follow(activity) - - if activity['type'] == 'Create': - return handle_incoming_create(activity) - - return HttpResponse() - - -def handle_account_search(query): - ''' webfingerin' other servers ''' - domain = query.split('@')[1] - try: - user = models.User.objects.get(username=query) - except models.User.DoesNotExist: - url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \ - (domain, query) - response = requests.get(url) - if not response.ok: - response.raise_for_status() - data = response.json() - for link in data['links']: - if link['rel'] == 'self': - user = get_or_create_remote_user(link['href']) - return user - - -def handle_incoming_shelve(activity): - ''' receiving an Add activity (to shelve a book) ''' - # TODO what happens here? If it's a remote over, then I think - # I should save both the activity and the ShelfBook entry. But - # I'll do that later. - uuid = activity['id'] - models.ShelveActivity.objects.get(uuid=uuid) - ''' - book_id = activity['object']['url'] - book = openlibrary.get_or_create_book(book_id) - user_ap_id = activity['actor'].replace('https//:', '') - user = models.User.objects.get(actor=user_ap_id) - if not user or not user.local: - return HttpResponseBadRequest() - - shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id']) - models.ShelfBook( - shelf=shelf, - book=book, - added_by=user, - ).save() - ''' - return HttpResponse() - - - -def handle_incoming_follow(activity): - ''' - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://friend.camp/768222ce-a1c7-479c-a544-c93b8b67fb54", - "type": "Follow", - "actor": "https://friend.camp/users/tripofmice", - "object": "https://ff2cb3e9.ngrok.io/api/u/mouse" - } - ''' - # figure out who they want to follow - to_follow = re.sub( - r'https?://([\w\.]+)/user/(\w+)', - r'\2@\1', - activity['object'] - ) - to_follow = models.User.objects.get(username=to_follow) - # figure out who they are - user = get_or_create_remote_user(activity['actor']) - to_follow.followers.add(user) - # verify uuid and accept the request - models.FollowActivity( - uuid=activity['id'], - user=user, - followed=to_follow, - content=activity, - activity_type='Follow', - ) - uuid = uuid4() - return JsonResponse({ - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'https://%s/%s' % (DOMAIN, uuid), - 'type': 'Accept', - 'actor': user.actor, - 'object': activity, - }) - - -def handle_outgoing_follow(user, to_follow): - ''' someone local wants to follow someone ''' - uuid = uuid4() - activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': str(uuid), - 'summary': '', - 'type': 'Follow', - 'actor': user.actor, - 'object': to_follow.actor, - } - - broadcast(user, activity, [format_inbox(to_follow)]) - -def handle_response(response): - ''' hopefully it's an accept from our follow request ''' - try: - activity = response.json() - except ValueError: - return - if activity['type'] == 'Accept': - handle_incoming_accept(activity) - -def handle_incoming_accept(activity): - ''' someone is accepting a follow request ''' - # not actually a remote user so this is kinda janky - user = get_or_create_remote_user(activity['actor']) - # the person our local user wants to follow, who said yes - followed = models.User.objects.get(actor=activity['object']['actor']) - followed.followers.add(user) - models.FollowActivity( - uuid=activity['id'], - user=user, - followed=followed, - content=activity, - ).save() - - -def handle_shelve(user, book, shelf): - ''' gettin organized ''' - # update the database - models.ShelfBook(book=book, shelf=shelf, added_by=user).save() - - # send out the activitypub action - summary = '%s marked %s as %s' % ( - user.username, - book.data['title'], - shelf.name - ) - - uuid = uuid4() - activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': str(uuid), - 'summary': summary, - 'type': 'Add', - 'actor': user.actor, - 'object': { - 'type': 'Document', - 'name': book.data['title'], - 'url': book.openlibrary_key - }, - 'target': { - 'type': 'Collection', - 'name': shelf.name, - 'id': shelf.activitypub_id - } - } - # TODO: this should be getting shared inboxes and deduplicating - recipients = [format_inbox(u) for u in user.followers.all()] - - models.ShelveActivity( - uuid=uuid, - user=user, - content=activity, - activity_type='Add', - shelf=shelf, - book=book, - ).save() - - broadcast(user, activity, recipients) - - -def handle_review(user, book, name, content, rating): - ''' post a review ''' - review_uuid = uuid4() - obj = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': str(review_uuid), - 'type': 'Article', - 'published': datetime.utcnow().isoformat(), - 'attributedTo': user.actor, - 'content': content, - 'inReplyTo': book.openlibrary_key, - 'rating': rating, # fedireads-only custom field - 'to': 'https://www.w3.org/ns/activitystreams#Public' - } - recipients = [format_inbox(u) for u in user.followers.all()] - create_uuid = uuid4() - activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - - 'id': str(create_uuid), - 'type': 'Create', - 'actor': user.actor, - - 'to': ['%s/followers' % user.actor], - 'cc': ['https://www.w3.org/ns/activitystreams#Public'], - - 'object': obj, - - } - - models.Review( - uuid=create_uuid, - user=user, - content=activity, - activity_type='Article', - book=book, - work=book.works.first(), - name=name, - rating=rating, - review_content=content, - ).save() - broadcast(user, activity, recipients) - - -def handle_incoming_create(activity): - ''' someone did something, good on them ''' - user = get_or_create_remote_user(activity['actor']) - uuid = activity['id'] - # if it's an article and in reply to a book, we have a review - if activity['object']['type'] == 'Article' and \ - 'inReplyTo' in activity['object']: - possible_book = activity['object']['inReplyTo'] - try: - book = get_or_create_book(possible_book) - models.Review( - uuid=uuid, - user=user, - content=activity, - activity_type='Article', - book=book, - work=book.works.first(), - name=activity['object']['name'], - rating=activity['object']['rating'], - review_content=activity['objet']['content'], - ).save() - return HttpResponse() - except KeyError: - pass - - models.Activity( - uuid=uuid, - user=user, - content=activity, - activity_type=activity['object']['type'] - ) - return HttpResponse() - -@csrf_exempt -def outbox(request, username): - ''' outbox for the requested user ''' - user = models.User.objects.get(localname=username) - size = models.Review.objects.filter(user=user).count() - if request.method == 'GET': - # list of activities - return JsonResponse({ - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': '%s/outbox' % user.actor, - 'type': 'OrderedCollection', - 'totalItems': size, - 'first': '%s/outbox?page=true' % user.actor, - 'last': '%s/outbox?min_id=0&page=true' % user.actor - }) - # TODO: paginated list of messages - - #data = request.body.decode('utf-8') - return HttpResponse() - - -def broadcast(sender, action, recipients): - ''' send out an event to all followers ''' - for recipient in recipients: - sign_and_send(sender, action, recipient) - - -def sign_and_send(sender, action, destination): - ''' crpyto whatever and http junk ''' - inbox_fragment = sender.inbox.replace('https://%s' % DOMAIN, '') - now = datetime.utcnow().isoformat() - message_to_sign = '''(request-target): post %s -host: https://%s -date: %s''' % (inbox_fragment, DOMAIN, now) - signer = pkcs1_15.new(RSA.import_key(sender.private_key)) - signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8'))) - - signature = 'keyId="%s",' % sender.localname - signature += 'headers="(request-target) host date",' - signature += 'signature="%s"' % b64encode(signed_message) - response = requests.post( - destination, - data=json.dumps(action), - headers={ - 'Date': now, - 'Signature': signature, - 'Host': DOMAIN, - }, - ) - if not response.ok: - response.raise_for_status() - handle_response(response) - - -def get_or_create_remote_user(actor): - ''' wow, a foreigner ''' - try: - user = models.User.objects.get(actor=actor) - except models.User.DoesNotExist: - # TODO: how do you actually correctly learn this? - username = '%s@%s' % (actor.split('/')[-1], actor.split('/')[2]) - user = models.User.objects.create_user( - username, - '', '', - actor=actor, - local=False - ) - return user - - -def format_inbox(user): - ''' describe an inbox ''' - return '%s/inbox' % (user.actor) diff --git a/fedireads/incoming.py b/fedireads/incoming.py new file mode 100644 index 000000000..a2163c09f --- /dev/null +++ b/fedireads/incoming.py @@ -0,0 +1,184 @@ +''' activitystream api ''' +from django.http import HttpResponse, HttpResponseBadRequest, \ + HttpResponseNotFound, JsonResponse +from django.views.decorators.csrf import csrf_exempt +from fedireads.settings import DOMAIN +from fedireads.openlibrary import get_or_create_book +from fedireads import models +from fedireads.api import get_or_create_remote_user +import json +from uuid import uuid4 + +def webfinger(request): + ''' allow other servers to ask about a user ''' + resource = request.GET.get('resource') + if not resource and not resource.startswith('acct:'): + return HttpResponseBadRequest() + ap_id = resource.replace('acct:', '') + user = models.User.objects.filter(username=ap_id).first() + if not user: + return HttpResponseNotFound('No account found') + return JsonResponse({ + 'subject': 'acct:%s' % (user.username), + 'links': [ + { + 'rel': 'self', + 'type': 'application/activity+json', + 'href': user.actor + } + ] + }) + + +@csrf_exempt +def get_actor(request, username): + ''' return an activitypub actor object ''' + if request.method != 'GET': + return HttpResponseBadRequest() + + user = models.User.objects.get(localname=username) + return JsonResponse({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + + 'id': user.actor, + 'type': 'Person', + 'preferredUsername': user.localname, + 'inbox': user.inbox, + 'followers': '%s/followers' % user.actor, + 'following': '%s/following' % user.actor, + 'summary': user.summary, + 'publicKey': { + 'id': '%s/#main-key' % user.actor, + 'owner': user.actor, + 'publicKeyPem': user.public_key, + }, + 'endpoints': { + 'sharedInbox': 'https://%s/inbox' % DOMAIN, + } + }) + + +@csrf_exempt +def inbox(request, username): + ''' incoming activitypub events ''' + if request.method == 'GET': + # TODO: return a collection of something? + return JsonResponse({}) + + # TODO: RSA key verification + + try: + activity = json.loads(request.body) + except json.decoder.JSONDecodeError: + return HttpResponseBadRequest + + # TODO: should do some kind of checking if the user accepts + # this action from the sender + # but this will just throw an error if the user doesn't exist I guess + models.User.objects.get(localname=username) + + if activity['type'] == 'Add': + return handle_incoming_shelve(activity) + + if activity['type'] == 'Follow': + return handle_incoming_follow(activity) + + if activity['type'] == 'Create': + return handle_incoming_create(activity) + + return HttpResponse() + + +def handle_incoming_shelve(activity): + ''' receiving an Add activity (to shelve a book) ''' + # TODO what happens here? If it's a remote over, then I think + # I should save both the activity and the ShelfBook entry. But + # I'll do that later. + uuid = activity['id'] + models.ShelveActivity.objects.get(uuid=uuid) + ''' + book_id = activity['object']['url'] + book = openlibrary.get_or_create_book(book_id) + user_ap_id = activity['actor'].replace('https//:', '') + user = models.User.objects.get(actor=user_ap_id) + if not user or not user.local: + return HttpResponseBadRequest() + + shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id']) + models.ShelfBook( + shelf=shelf, + book=book, + added_by=user, + ).save() + ''' + return HttpResponse() + + +def handle_incoming_follow(activity): + ''' + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://friend.camp/768222ce-a1c7-479c-a544-c93b8b67fb54", + "type": "Follow", + "actor": "https://friend.camp/users/tripofmice", + "object": "https://ff2cb3e9.ngrok.io/api/u/mouse" + } + ''' + # figure out who they want to follow + to_follow = models.User.objects.get(actor=activity['object']) + # figure out who they are + user = get_or_create_remote_user(activity['actor']) + to_follow.followers.add(user) + # verify uuid and accept the request + models.FollowActivity( + uuid=activity['id'], + user=user, + followed=to_follow, + content=activity, + activity_type='Follow', + ) + uuid = uuid4() + return JsonResponse({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': 'https://%s/%s' % (DOMAIN, uuid), + 'type': 'Accept', + 'actor': user.actor, + 'object': activity, + }) + + +def handle_incoming_create(activity): + ''' someone did something, good on them ''' + user = get_or_create_remote_user(activity['actor']) + uuid = activity['id'] + # if it's an article and in reply to a book, we have a review + if activity['object']['type'] == 'Article' and \ + 'inReplyTo' in activity['object']: + possible_book = activity['object']['inReplyTo'] + try: + book = get_or_create_book(possible_book) + models.Review( + uuid=uuid, + user=user, + content=activity, + activity_type='Article', + book=book, + work=book.works.first(), + name=activity['object']['name'], + rating=activity['object']['rating'], + review_content=activity['objet']['content'], + ).save() + return HttpResponse() + except KeyError: + pass + + models.Activity( + uuid=uuid, + user=user, + content=activity, + activity_type=activity['object']['type'] + ) + return HttpResponse() diff --git a/fedireads/migrations/0001_initial.py b/fedireads/migrations/0001_initial.py index beda8d365..9b0c59e2f 100644 --- a/fedireads/migrations/0001_initial.py +++ b/fedireads/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.2 on 2020-01-28 08:28 +# Generated by Django 3.0.2 on 2020-01-28 09:04 from django.conf import settings import django.contrib.auth.models diff --git a/fedireads/models.py b/fedireads/models.py index 0b030029f..1f21aafe6 100644 --- a/fedireads/models.py +++ b/fedireads/models.py @@ -34,9 +34,17 @@ class User(AbstractUser): @receiver(models.signals.pre_save, sender=User) def execute_before_save(sender, instance, *args, **kwargs): ''' create shelves for new users ''' - # TODO: how are remote users handled? what if they aren't readers? - if not instance.local or instance.id: + # this user already exists, no need to poplate fields + if instance.id: return + + # TODO: how do I know this properly??? + if not instance.local: + instance.inbox = instance.actor = 'inbox' + instance.outbox = instance.actor = 'outbox' + return + + # populate fields for local users instance.localname = instance.username instance.username = '%s@%s' % (instance.username, DOMAIN) instance.actor = 'https://%s/user/%s' % (DOMAIN, instance.localname) diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py new file mode 100644 index 000000000..2b4dffc71 --- /dev/null +++ b/fedireads/outgoing.py @@ -0,0 +1,214 @@ +''' activitystream api ''' +from base64 import b64encode +from Crypto.PublicKey import RSA +from Crypto.Signature import pkcs1_15 +from Crypto.Hash import SHA256 +from datetime import datetime +from django.http import HttpResponse, JsonResponse +from django.views.decorators.csrf import csrf_exempt +from fedireads.settings import DOMAIN +from fedireads import models +from fedireads.api import get_or_create_remote_user +import json +import requests +from uuid import uuid4 + + +def handle_account_search(query): + ''' webfingerin' other servers ''' + domain = query.split('@')[1] + try: + user = models.User.objects.get(username=query) + except models.User.DoesNotExist: + url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \ + (domain, query) + response = requests.get(url) + if not response.ok: + response.raise_for_status() + data = response.json() + for link in data['links']: + if link['rel'] == 'self': + user = get_or_create_remote_user(link['href']) + return user + + +def handle_outgoing_follow(user, to_follow): + ''' someone local wants to follow someone ''' + uuid = uuid4() + activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': str(uuid), + 'summary': '', + 'type': 'Follow', + 'actor': user.actor, + 'object': to_follow.actor, + } + + broadcast(user, activity, [to_follow.inbox]) + + +def handle_response(response): + ''' hopefully it's an accept from our follow request ''' + try: + activity = response.json() + except ValueError: + return + if activity['type'] == 'Accept': + handle_incoming_accept(activity) + +def handle_incoming_accept(activity): + ''' someone is accepting a follow request ''' + # not actually a remote user so this is kinda janky + user = get_or_create_remote_user(activity['actor']) + # the person our local user wants to follow, who said yes + followed = models.User.objects.get(actor=activity['object']['actor']) + followed.followers.add(user) + models.FollowActivity( + uuid=activity['id'], + user=user, + followed=followed, + content=activity, + ).save() + + +def handle_shelve(user, book, shelf): + ''' gettin organized ''' + # update the database + models.ShelfBook(book=book, shelf=shelf, added_by=user).save() + + # send out the activitypub action + summary = '%s marked %s as %s' % ( + user.username, + book.data['title'], + shelf.name + ) + + uuid = uuid4() + activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': str(uuid), + 'summary': summary, + 'type': 'Add', + 'actor': user.actor, + 'object': { + 'type': 'Document', + 'name': book.data['title'], + 'url': book.openlibrary_key + }, + 'target': { + 'type': 'Collection', + 'name': shelf.name, + 'id': shelf.activitypub_id + } + } + # TODO: this should be getting shared inboxes and deduplicating + recipients = [u.inbox for u in user.followers.all()] + + models.ShelveActivity( + uuid=uuid, + user=user, + content=activity, + activity_type='Add', + shelf=shelf, + book=book, + ).save() + + broadcast(user, activity, recipients) + + +def handle_review(user, book, name, content, rating): + ''' post a review ''' + review_uuid = uuid4() + obj = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': str(review_uuid), + 'type': 'Article', + 'published': datetime.utcnow().isoformat(), + 'attributedTo': user.actor, + 'content': content, + 'inReplyTo': book.openlibrary_key, + 'rating': rating, # fedireads-only custom field + 'to': 'https://www.w3.org/ns/activitystreams#Public' + } + recipients = [u.inbox for u in user.followers.all()] + create_uuid = uuid4() + activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + + 'id': str(create_uuid), + 'type': 'Create', + 'actor': user.actor, + + 'to': ['%s/followers' % user.actor], + 'cc': ['https://www.w3.org/ns/activitystreams#Public'], + + 'object': obj, + + } + + models.Review( + uuid=create_uuid, + user=user, + content=activity, + activity_type='Article', + book=book, + work=book.works.first(), + name=name, + rating=rating, + review_content=content, + ).save() + broadcast(user, activity, recipients) + + +@csrf_exempt +def outbox(request, username): + ''' outbox for the requested user ''' + user = models.User.objects.get(localname=username) + size = models.Review.objects.filter(user=user).count() + if request.method == 'GET': + # list of activities + return JsonResponse({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': '%s/outbox' % user.actor, + 'type': 'OrderedCollection', + 'totalItems': size, + 'first': '%s/outbox?page=true' % user.actor, + 'last': '%s/outbox?min_id=0&page=true' % user.actor + }) + # TODO: paginated list of messages + + #data = request.body.decode('utf-8') + return HttpResponse() + + +def broadcast(sender, action, recipients): + ''' send out an event to all followers ''' + for recipient in recipients: + sign_and_send(sender, action, recipient) + + +def sign_and_send(sender, action, destination): + ''' crpyto whatever and http junk ''' + inbox_fragment = sender.inbox.replace('https://%s' % DOMAIN, '') + now = datetime.utcnow().isoformat() + message_to_sign = '''(request-target): post %s +host: https://%s +date: %s''' % (inbox_fragment, DOMAIN, now) + signer = pkcs1_15.new(RSA.import_key(sender.private_key)) + signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8'))) + + signature = 'keyId="%s",' % sender.localname + signature += 'headers="(request-target) host date",' + signature += 'signature="%s"' % b64encode(signed_message) + response = requests.post( + destination, + data=json.dumps(action), + headers={ + 'Date': now, + 'Signature': signature, + 'Host': DOMAIN, + }, + ) + if not response.ok: + response.raise_for_status() + handle_response(response) diff --git a/fedireads/urls.py b/fedireads/urls.py index 9b8fb3b45..0bed8aa40 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -15,17 +15,17 @@ Including another URLconf """ from django.contrib import admin from django.urls import path -from fedireads import federation, views, settings +from fedireads import incoming, outgoing, views, settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), # federation endpoints - path('user/.json', federation.get_actor), - path('user//inbox', federation.inbox), - path('user//outbox', federation.outbox), - path('.well-known/webfinger', federation.webfinger), + path('user/.json', incoming.get_actor), + path('user//inbox', incoming.inbox), + path('user//outbox', outgoing.outbox), + path('.well-known/webfinger', incoming.webfinger), # ui views path('', views.home), diff --git a/fedireads/views.py b/fedireads/views.py index c082386a2..42f515f7c 100644 --- a/fedireads/views.py +++ b/fedireads/views.py @@ -7,7 +7,7 @@ from django.template.response import TemplateResponse from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponseNotFound from fedireads import models, openlibrary -from fedireads import federation as api +from fedireads import outgoing as api from fedireads.settings import DOMAIN import re