mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-22 16:16:39 +00:00
Followers and following lists
This commit is contained in:
parent
77bab24834
commit
a9d938fbb2
6 changed files with 169 additions and 39 deletions
|
@ -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) '''
|
||||
|
|
|
@ -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=[
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
Loading…
Reference in a new issue