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
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) '''

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
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=[

View file

@ -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)

View file

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

View file

@ -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/<str:username>.json', incoming.get_actor),
path('user/<str:username>/inbox', incoming.inbox),
path('user/<str:username>/outbox', outgoing.outbox),
path('.well-known/webfinger', incoming.webfinger),
re_path(r'^inbox/?$', incoming.shared_inbox),
re_path(r'^user/(?P<username>\w+).json/?$', incoming.get_actor),
re_path(r'^user/(?P<username>\w+)/inbox/?$', incoming.inbox),
re_path(r'^user/(?P<username>\w+)/outbox/?$', outgoing.outbox),
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
path('', views.home),
path('register/', views.register),
path('login/', views.user_login),
path('logout/', views.user_logout),
path('user/<str:username>', views.user_profile),
path('user/<str:username>/edit/', views.user_profile_edit),
path('work/<str:book_identifier>', 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<username>[\w@\.]+)/?$', views.user_profile),
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
path('review/', views.review),
path('shelve/<str:shelf_id>/<int:book_id>', 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<shelf_id>\w+)/(?P<book_id>\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)

View file

@ -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'