Followers and following lists

This commit is contained in:
Mouse Reeve 2020-01-29 11:45:19 -08:00
parent 77bab24834
commit a9d938fbb2
6 changed files with 169 additions and 39 deletions

View file

@ -32,6 +32,12 @@ def webfinger(request):
}) })
'''
def host_meta(request):
import pdb;pdb.set_trace()
'''
@csrf_exempt @csrf_exempt
def shared_inbox(request): def shared_inbox(request):
''' incoming activitypub events ''' ''' incoming activitypub events '''
@ -104,6 +110,7 @@ def get_actor(request, username):
'id': user.actor, 'id': user.actor,
'type': 'Person', 'type': 'Person',
'preferredUsername': user.localname, 'preferredUsername': user.localname,
'name': user.name,
'inbox': user.inbox, 'inbox': user.inbox,
'followers': '%s/followers' % user.actor, 'followers': '%s/followers' % user.actor,
'following': '%s/following' % 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): def handle_incoming_shelve(activity):
''' receiving an Add activity (to shelve a book) ''' ''' receiving an Add activity (to shelve a book) '''

View file

@ -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 from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
@ -41,14 +41,11 @@ class Migration(migrations.Migration):
('outbox', models.CharField(max_length=255, unique=True)), ('outbox', models.CharField(max_length=255, unique=True)),
('summary', models.TextField(blank=True, null=True)), ('summary', models.TextField(blank=True, null=True)),
('local', models.BooleanField(default=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)), ('name', models.CharField(blank=True, max_length=100, null=True)),
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')), ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
('created_date', models.DateTimeField(auto_now_add=True)), ('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=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={ options={
'verbose_name': 'user', 'verbose_name': 'user',
@ -96,6 +93,16 @@ class Migration(migrations.Migration):
('authors', models.ManyToManyField(to='fedireads.Author')), ('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( migrations.CreateModel(
name='Shelf', name='Shelf',
fields=[ fields=[
@ -144,6 +151,26 @@ class Migration(migrations.Migration):
name='shelves', name='shelves',
field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Shelf'), 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( migrations.CreateModel(
name='ShelveActivity', name='ShelveActivity',
fields=[ fields=[

View file

@ -19,19 +19,22 @@ class User(AbstractUser):
actor = models.CharField(max_length=255, unique=True) actor = models.CharField(max_length=255, unique=True)
inbox = models.CharField(max_length=255, unique=True) inbox = models.CharField(max_length=255, unique=True)
shared_inbox = models.CharField(max_length=255) 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) outbox = models.CharField(max_length=255, unique=True)
summary = models.TextField(blank=True, null=True) summary = models.TextField(blank=True, null=True)
local = models.BooleanField(default=True) local = models.BooleanField(default=True)
localname = models.CharField( localname = models.CharField(
max_length=255, max_length=255,
null=True, null=True,
blank=True,
unique=True unique=True
) )
# name is your display name, which you can change at will # name is your display name, which you can change at will
name = models.CharField(max_length=100, blank=True, null=True) name = models.CharField(max_length=100, blank=True, null=True)
avatar = models.ImageField(upload_to='avatars/', 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) followers = models.ManyToManyField('self', symmetrical=False)
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
@ -91,6 +94,16 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
).save() ).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): class Activity(models.Model):
''' basic fields for storing activities ''' ''' basic fields for storing activities '''
uuid = models.CharField(max_length=255, unique=True) uuid = models.CharField(max_length=255, unique=True)

View file

@ -2,11 +2,13 @@
{% block content %} {% block content %}
<div id="content"> <div id="content">
{% for result in results %} {% for result in results %}
{{ result.username }} <div>
<form action="/follow/" method="post"> <h2>{{ result.username }}</h2>
<input type="hidden" name="user" value="{{ result.id }}"></input> <form action="/follow/" method="post">
<input type="submit" value="Follow"></input> <input type="hidden" name="user" value="{{ result.id }}"></input>
</form> <input type="submit" value="Follow"></input>
</form>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,7 @@
''' url routing for the app and api ''' ''' url routing for the app and api '''
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, re_path
from fedireads import incoming, outgoing, views, settings from fedireads import incoming, outgoing, views, settings
@ -10,27 +10,31 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
# federation endpoints # federation endpoints
path('inbox', incoming.shared_inbox), re_path(r'^inbox/?$', incoming.shared_inbox),
path('user/<str:username>.json', incoming.get_actor), re_path(r'^user/(?P<username>\w+).json/?$', incoming.get_actor),
path('user/<str:username>/inbox', incoming.inbox), re_path(r'^user/(?P<username>\w+)/inbox/?$', incoming.inbox),
path('user/<str:username>/outbox', outgoing.outbox), re_path(r'^user/(?P<username>\w+)/outbox/?$', outgoing.outbox),
path('.well-known/webfinger', incoming.webfinger), re_path(r'^user/(?P<username>\w+)/followers/?$', incoming.get_followers),
re_path(r'^user/(?P<username>\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 # ui views
path('', views.home), re_path(r'^/?$', views.home),
path('register/', views.register), re_path(r'^register/?$', views.register),
path('login/', views.user_login), re_path(r'^login/?$', views.user_login),
path('logout/', views.user_logout), re_path(r'^logout/?$', views.user_logout),
path('user/<str:username>', views.user_profile), # this endpoint is both ui and fed depending on Accept type
path('user/<str:username>/edit/', views.user_profile_edit), re_path(r'^user/(?P<username>[\w@\.]+)/?$', views.user_profile),
path('work/<str:book_identifier>', views.book_page), re_path(r'^user/(?P<username>\w+)/edit/?$', views.user_profile_edit),
re_path(r'^work/(?P<book_identifier>\w+)/?$', views.book_page),
# internal action endpoints # internal action endpoints
path('review/', views.review), re_path(r'^review/?$', views.review),
path('shelve/<str:shelf_id>/<int:book_id>', views.shelve), re_path(r'^shelve/(?P<shelf_id>\w+)/(?P<book_id>\d+)/?$', views.shelve),
path('follow/', views.follow), re_path(r'^follow/?$', views.follow),
path('unfollow/', views.unfollow), re_path(r'^unfollow/?$', views.unfollow),
path('search/', views.search), re_path(r'^search/?$', views.search),
path('edit_profile/', views.edit_profile), re_path(r'^edit_profile/?$', views.edit_profile),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -8,7 +8,7 @@ from django.template.response import TemplateResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
import re import re
from fedireads import forms, models, openlibrary, outgoing as api from fedireads import forms, models, openlibrary, outgoing, incoming
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
@ -107,9 +107,14 @@ def register(request):
return redirect('/') return redirect('/')
@login_required
def user_profile(request, username): def user_profile(request, username):
''' profile page for a user ''' ''' 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: try:
user = models.User.objects.get(localname=username) user = models.User.objects.get(localname=username)
except models.User.DoesNotExist: 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) desired_shelf = models.Shelf.objects.get(identifier=shelf_id)
if reshelve: if reshelve:
try: try:
current_shelf = models.Shelf.objects.get(user=request.user, book=book) current_shelf = models.Shelf.objects.get(
api.handle_unshelve(request.user, book, current_shelf) user=request.user,
book=book
)
outgoing.handle_unshelve(request.user, book, current_shelf)
except models.Shelf.DoesNotExist: except models.Shelf.DoesNotExist:
pass pass
api.handle_shelve(request.user, book, desired_shelf) outgoing.handle_shelve(request.user, book, desired_shelf)
return redirect('/') return redirect('/')
@ -208,7 +216,7 @@ def review(request):
content = form.data.get('review_content') content = form.data.get('review_content')
rating = form.data.get('rating') 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) return redirect(book_identifier)
@ -220,7 +228,7 @@ def follow(request):
# should this be an actor rather than an id? idk # should this be an actor rather than an id? idk
to_follow = models.User.objects.get(id=to_follow) 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) return redirect('/user/%s' % to_follow.username)
@ -241,9 +249,11 @@ def search(request):
''' that search bar up top ''' ''' that search bar up top '''
query = request.GET.get('q') query = request.GET.get('q')
if re.match(r'\w+@\w+.\w+', query): 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' template = 'user_results.html'
else: else:
# just send the question over to openlibrary for book search
results = openlibrary.book_search(query) results = openlibrary.book_search(query)
template = 'book_results.html' template = 'book_results.html'