diff --git a/fedireads/incoming.py b/fedireads/incoming.py index bb7b0fca4..04f590c1e 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -32,6 +32,12 @@ def webfinger(request): }) +''' +def host_meta(request): + import pdb;pdb.set_trace() +''' + + @csrf_exempt def shared_inbox(request): ''' incoming activitypub events ''' @@ -104,6 +110,7 @@ def get_actor(request, username): 'id': user.actor, 'type': 'Person', 'preferredUsername': user.localname, + 'name': user.name, 'inbox': user.inbox, 'followers': '%s/followers' % user.actor, 'following': '%s/following' % user.actor, @@ -118,6 +125,73 @@ def get_actor(request, username): } }) +@csrf_exempt +def get_followers(request, username): + ''' return a list of followers for an actor ''' + if request.method != 'GET': + return HttpResponseBadRequest() + + user = models.User.objects.get(localname=username) + followers = user.followers + id_slug = '%s/followers' % user.actor + if request.GET.get('page'): + page = request.GET.get('page') + return JsonResponse(get_follow_page(followers, id_slug, page)) + follower_count = followers.count() + return JsonResponse({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': id_slug, + 'type': 'OrderedCollection', + 'totalItems': follower_count, + 'first': '%s?page=1' % id_slug, + }) + + +@csrf_exempt +def get_following(request, username): + ''' return a list of following for an actor ''' + # TODO: this is total deplication of get_followers, should be streamlined + if request.method != 'GET': + return HttpResponseBadRequest() + + user = models.User.objects.get(localname=username) + following = models.User.objects.filter(followers=user) + id_slug = '%s/following' % user.actor + if request.GET.get('page'): + page = request.GET.get('page') + return JsonResponse(get_follow_page(following, id_slug, page)) + following_count = following.count() + return JsonResponse({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': id_slug, + 'type': 'OrderedCollection', + 'totalItems': following_count, + 'first': '%s?page=1' % id_slug, + }) + + +def get_follow_page(user_list, id_slug, page): + ''' format a list of followers/following ''' + page = int(page) + page_length = 10 + start = (page - 1) * page_length + end = start + page_length + follower_page = user_list.all()[start:end] + data = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': '%s?page=%d' % (id_slug, page), + 'type': 'OrderedCollectionPage', + 'totalItems': user_list.count(), + 'partOf': id_slug, + 'orderedItems': [u.actor for u in follower_page], + } + if end <= user_list.count(): + # there are still more pages + data['next'] = '%s?page=%d' % (id_slug, page + 1) + if start > 0: + data['prev'] = '%s?page=%d' % (id_slug, page - 1) + return data + def handle_incoming_shelve(activity): ''' receiving an Add activity (to shelve a book) ''' diff --git a/fedireads/migrations/0001_initial.py b/fedireads/migrations/0001_initial.py index 962fab954..043a3fc47 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-29 09:04 +# Generated by Django 3.0.2 on 2020-01-29 18:40 from django.conf import settings import django.contrib.auth.models @@ -41,14 +41,11 @@ class Migration(migrations.Migration): ('outbox', models.CharField(max_length=255, unique=True)), ('summary', models.TextField(blank=True, null=True)), ('local', models.BooleanField(default=True)), - ('localname', models.CharField(blank=True, max_length=255, null=True, unique=True)), + ('localname', models.CharField(max_length=255, null=True, unique=True)), ('name', models.CharField(blank=True, max_length=100, null=True)), ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')), ('created_date', models.DateTimeField(auto_now_add=True)), ('updated_date', models.DateTimeField(auto_now=True)), - ('followers', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ], options={ 'verbose_name': 'user', @@ -96,6 +93,16 @@ class Migration(migrations.Migration): ('authors', models.ManyToManyField(to='fedireads.Author')), ], ), + migrations.CreateModel( + name='FederatedServer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('server_name', models.CharField(max_length=255, unique=True)), + ('shared_inbox', models.CharField(max_length=255, unique=True)), + ('status', models.CharField(default='federated', max_length=255)), + ('application_type', models.CharField(max_length=255, null=True)), + ], + ), migrations.CreateModel( name='Shelf', fields=[ @@ -144,6 +151,26 @@ class Migration(migrations.Migration): name='shelves', field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Shelf'), ), + migrations.AddField( + model_name='user', + name='federated_server', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.FederatedServer'), + ), + migrations.AddField( + model_name='user', + name='followers', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), + ), + migrations.AddField( + model_name='user', + name='user_permissions', + field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), + ), migrations.CreateModel( name='ShelveActivity', fields=[ diff --git a/fedireads/models.py b/fedireads/models.py index 47783ad1b..1161b331d 100644 --- a/fedireads/models.py +++ b/fedireads/models.py @@ -19,19 +19,22 @@ class User(AbstractUser): actor = models.CharField(max_length=255, unique=True) inbox = models.CharField(max_length=255, unique=True) shared_inbox = models.CharField(max_length=255) + federated_server = models.ForeignKey( + 'FederatedServer', + on_delete=models.PROTECT, + null=True, + ) outbox = models.CharField(max_length=255, unique=True) summary = models.TextField(blank=True, null=True) local = models.BooleanField(default=True) localname = models.CharField( max_length=255, null=True, - blank=True, unique=True ) # name is your display name, which you can change at will name = models.CharField(max_length=100, blank=True, null=True) avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) - # TODO: a field for if non-local users are readers or others followers = models.ManyToManyField('self', symmetrical=False) created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) @@ -91,6 +94,16 @@ def execute_after_save(sender, instance, created, *args, **kwargs): ).save() +class FederatedServer(models.Model): + ''' store which server's we federate with ''' + server_name = models.CharField(max_length=255, unique=True) + shared_inbox = models.CharField(max_length=255, unique=True) + # federated, blocked, whatever else + status = models.CharField(max_length=255, default='federated') + # is it mastodon, fedireads, etc + application_type = models.CharField(max_length=255, null=True) + + class Activity(models.Model): ''' basic fields for storing activities ''' uuid = models.CharField(max_length=255, unique=True) diff --git a/fedireads/templates/user_results.html b/fedireads/templates/user_results.html index 1ab735731..5f2d41354 100644 --- a/fedireads/templates/user_results.html +++ b/fedireads/templates/user_results.html @@ -2,11 +2,13 @@ {% block content %}
{% for result in results %} - {{ result.username }} -
- - -
+
+

{{ result.username }}

+
+ + +
+
{% endfor %}
{% endblock %} diff --git a/fedireads/urls.py b/fedireads/urls.py index c551871f3..fd40b4341 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -1,7 +1,7 @@ ''' url routing for the app and api ''' from django.conf.urls.static import static from django.contrib import admin -from django.urls import path +from django.urls import path, re_path from fedireads import incoming, outgoing, views, settings @@ -10,27 +10,31 @@ urlpatterns = [ path('admin/', admin.site.urls), # federation endpoints - path('inbox', incoming.shared_inbox), - path('user/.json', incoming.get_actor), - path('user//inbox', incoming.inbox), - path('user//outbox', outgoing.outbox), - path('.well-known/webfinger', incoming.webfinger), + re_path(r'^inbox/?$', incoming.shared_inbox), + re_path(r'^user/(?P\w+).json/?$', incoming.get_actor), + re_path(r'^user/(?P\w+)/inbox/?$', incoming.inbox), + re_path(r'^user/(?P\w+)/outbox/?$', outgoing.outbox), + re_path(r'^user/(?P\w+)/followers/?$', incoming.get_followers), + re_path(r'^user/(?P\w+)/following/?$', incoming.get_following), + re_path(r'^.well-known/webfinger/?$', incoming.webfinger), + # TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta), # ui views - path('', views.home), - path('register/', views.register), - path('login/', views.user_login), - path('logout/', views.user_logout), - path('user/', views.user_profile), - path('user//edit/', views.user_profile_edit), - path('work/', views.book_page), + re_path(r'^/?$', views.home), + re_path(r'^register/?$', views.register), + re_path(r'^login/?$', views.user_login), + re_path(r'^logout/?$', views.user_logout), + # this endpoint is both ui and fed depending on Accept type + re_path(r'^user/(?P[\w@\.]+)/?$', views.user_profile), + re_path(r'^user/(?P\w+)/edit/?$', views.user_profile_edit), + re_path(r'^work/(?P\w+)/?$', views.book_page), # internal action endpoints - path('review/', views.review), - path('shelve//', views.shelve), - path('follow/', views.follow), - path('unfollow/', views.unfollow), - path('search/', views.search), - path('edit_profile/', views.edit_profile), + re_path(r'^review/?$', views.review), + re_path(r'^shelve/(?P\w+)/(?P\d+)/?$', views.shelve), + re_path(r'^follow/?$', views.follow), + re_path(r'^unfollow/?$', views.unfollow), + re_path(r'^search/?$', views.search), + re_path(r'^edit_profile/?$', views.edit_profile), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/fedireads/views.py b/fedireads/views.py index 55c603b8c..517751965 100644 --- a/fedireads/views.py +++ b/fedireads/views.py @@ -8,7 +8,7 @@ from django.template.response import TemplateResponse from django.views.decorators.csrf import csrf_exempt import re -from fedireads import forms, models, openlibrary, outgoing as api +from fedireads import forms, models, openlibrary, outgoing, incoming from fedireads.settings import DOMAIN @@ -107,9 +107,14 @@ def register(request): return redirect('/') -@login_required def user_profile(request, username): ''' profile page for a user ''' + content = request.headers.get('Accept') + if 'json' in content: + # we have a json request + return incoming.get_actor(request, username) + + # otherwise we're at a UI view try: user = models.User.objects.get(localname=username) except models.User.DoesNotExist: @@ -184,11 +189,14 @@ def shelve(request, shelf_id, book_id, reshelve=True): desired_shelf = models.Shelf.objects.get(identifier=shelf_id) if reshelve: try: - current_shelf = models.Shelf.objects.get(user=request.user, book=book) - api.handle_unshelve(request.user, book, current_shelf) + current_shelf = models.Shelf.objects.get( + user=request.user, + book=book + ) + outgoing.handle_unshelve(request.user, book, current_shelf) except models.Shelf.DoesNotExist: pass - api.handle_shelve(request.user, book, desired_shelf) + outgoing.handle_shelve(request.user, book, desired_shelf) return redirect('/') @@ -208,7 +216,7 @@ def review(request): content = form.data.get('review_content') rating = form.data.get('rating') - api.handle_review(request.user, book, name, content, rating) + outgoing.handle_review(request.user, book, name, content, rating) return redirect(book_identifier) @@ -220,7 +228,7 @@ def follow(request): # should this be an actor rather than an id? idk to_follow = models.User.objects.get(id=to_follow) - api.handle_outgoing_follow(request.user, to_follow) + outgoing.handle_outgoing_follow(request.user, to_follow) return redirect('/user/%s' % to_follow.username) @@ -241,9 +249,11 @@ def search(request): ''' that search bar up top ''' query = request.GET.get('q') if re.match(r'\w+@\w+.\w+', query): - results = [api.handle_account_search(query)] + # if something looks like a username, search with webfinger + results = [outgoing.handle_account_search(query)] template = 'user_results.html' else: + # just send the question over to openlibrary for book search results = openlibrary.book_search(query) template = 'book_results.html'