From 68813f9453af409b9f9470f7cfe2efbd1c3a0175 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 5 Oct 2020 14:17:04 -0700
Subject: [PATCH 001/416] Nginx and certbot config for prod deploy
---
nginx/Dockerfile | 4 --
nginx/{nginx.conf => default.conf} | 0
nginx/prod-default.conf | 44 ++++++++++++++++++
prod-docker-compose.yml | 74 ++++++++++++++++++++++++++++++
4 files changed, 118 insertions(+), 4 deletions(-)
delete mode 100644 nginx/Dockerfile
rename nginx/{nginx.conf => default.conf} (100%)
create mode 100644 nginx/prod-default.conf
create mode 100644 prod-docker-compose.yml
diff --git a/nginx/Dockerfile b/nginx/Dockerfile
deleted file mode 100644
index 66074cf6..00000000
--- a/nginx/Dockerfile
+++ /dev/null
@@ -1,4 +0,0 @@
-FROM nginx:1.17.4-alpine
-
-RUN rm /etc/nginx/conf.d/default.conf
-COPY nginx.conf /etc/nginx/conf.d
diff --git a/nginx/nginx.conf b/nginx/default.conf
similarity index 100%
rename from nginx/nginx.conf
rename to nginx/default.conf
diff --git a/nginx/prod-default.conf b/nginx/prod-default.conf
new file mode 100644
index 00000000..079a7aaf
--- /dev/null
+++ b/nginx/prod-default.conf
@@ -0,0 +1,44 @@
+upstream web {
+ server web:8000;
+}
+
+server {
+ listen [::]:80;
+ listen 80;
+
+ server_name bookwyrm.social www.bookwyrm.social;
+
+ location ~ /.well-known/acme-challenge {
+ allow all;
+ root /var/www/certbot;
+ }
+
+ # redirect http to https www
+ return 301 https://www.bookwyrm.social$request_uri;
+}
+
+server {
+ listen [::]:443 ssl http2;
+ listen 443 ssl http2;
+
+ server_name bookwyrm.social;
+
+ # SSL code
+ ssl_certificate /etc/nginx/ssl/live/bookwyrm.social/fullchain.pem;
+ ssl_certificate_key /etc/nginx/ssl/live/bookwyrm.social/privkey.pem;
+
+ location / {
+ proxy_pass http://web;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_redirect off;
+ }
+
+ location /images/ {
+ alias /app/images/;
+ }
+
+ location /static/ {
+ alias /app/static/;
+ }
+}
diff --git a/prod-docker-compose.yml b/prod-docker-compose.yml
new file mode 100644
index 00000000..5f5ac9c6
--- /dev/null
+++ b/prod-docker-compose.yml
@@ -0,0 +1,74 @@
+version: '3'
+
+services:
+ nginx:
+ image: nginx:latest
+ ports:
+ - 80:80
+ - 443:443
+ depends_on:
+ - web
+ networks:
+ - main
+ volumes:
+ - ./nginx:/etc/nginx/conf.d
+ - ./certbot/conf:/etc/nginx/ssl
+ - ./certbot/data:/var/www/certbot
+ - static_volume:/app/static
+ - media_volume:/app/images
+ certbot:
+ image: certbot/certbot:latest
+ command: certonly --webroot --webroot-path=/var/www/certbot --email mouse.reeve@gmail.com --agree-tos --no-eff-email -d bookwyrm.social -d www.bookwyrm.social
+ volumes:
+ - ./certbot/conf:/etc/letsencrypt
+ - ./certbot/logs:/var/log/letsencrypt
+ - ./certbot/data:/var/www/certbot
+ db:
+ image: postgres
+ env_file: .env
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ networks:
+ - main
+ web:
+ build: .
+ command: python manage.py runserver 0.0.0.0:8000
+ volumes:
+ - .:/app
+ - static_volume:/app/static
+ - media_volume:/app/images
+ depends_on:
+ - db
+ - celery_worker
+ networks:
+ - main
+ ports:
+ - 8000:8000
+ redis:
+ image: redis
+ env_file: .env
+ ports:
+ - "6379:6379"
+ networks:
+ - main
+ restart: on-failure
+ celery_worker:
+ env_file: .env
+ build: .
+ networks:
+ - main
+ command: celery -A celerywyrm worker -l info
+ volumes:
+ - .:/app
+ - static_volume:/app/static
+ - media_volume:/app/images
+ depends_on:
+ - db
+ - redis
+ restart: on-failure
+volumes:
+ pgdata:
+ static_volume:
+ media_volume:
+networks:
+ main:
From e24eca7da0a8b49c22f2e650ce0cf918cf53d713 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 5 Oct 2020 14:22:37 -0700
Subject: [PATCH 002/416] Config files for prod deployment
---
docker-compose.yml | 3 ++-
nginx/default.conf | 43 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 6f9dbdc2..d7c4ec3b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@ version: '3'
services:
nginx:
- build: ./nginx
+ image: nginx:latest
ports:
- 1333:80
depends_on:
@@ -10,6 +10,7 @@ services:
networks:
- main
volumes:
+ - ./nginx:/etc/nginx/conf.d
- static_volume:/app/static
- media_volume:/app/images
db:
diff --git a/nginx/default.conf b/nginx/default.conf
index d3898287..396852e2 100644
--- a/nginx/default.conf
+++ b/nginx/default.conf
@@ -20,3 +20,46 @@ server {
alias /app/static/;
}
}
+
+# PROD version
+#
+#server {
+# listen [::]:80;
+# listen 80;
+#
+# server_name you-domain.com www.you-domain.com;
+#
+# location ~ /.well-known/acme-challenge {
+# allow all;
+# root /var/www/certbot;
+# }
+#
+# # redirect http to https www
+# return 301 https://www.you-domain.com$request_uri;
+#}
+#
+#server {
+# listen [::]:443 ssl http2;
+# listen 443 ssl http2;
+#
+# server_name you-domain.com;
+#
+# # SSL code
+# ssl_certificate /etc/nginx/ssl/live/you-domain.com/fullchain.pem;
+# ssl_certificate_key /etc/nginx/ssl/live/you-domain.com/privkey.pem;
+#
+# location / {
+# proxy_pass http://web;
+# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+# proxy_set_header Host $host;
+# proxy_redirect off;
+# }
+#
+# location /images/ {
+# alias /app/images/;
+# }
+#
+# location /static/ {
+# alias /app/static/;
+# }
+#}
From d29ed2746ab03424900e451bee7f8f5c44d752fb Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 5 Oct 2020 14:24:14 -0700
Subject: [PATCH 003/416] Removed old prod nginx conf
---
nginx/prod-default.conf | 44 -----------------------------------------
1 file changed, 44 deletions(-)
delete mode 100644 nginx/prod-default.conf
diff --git a/nginx/prod-default.conf b/nginx/prod-default.conf
deleted file mode 100644
index 079a7aaf..00000000
--- a/nginx/prod-default.conf
+++ /dev/null
@@ -1,44 +0,0 @@
-upstream web {
- server web:8000;
-}
-
-server {
- listen [::]:80;
- listen 80;
-
- server_name bookwyrm.social www.bookwyrm.social;
-
- location ~ /.well-known/acme-challenge {
- allow all;
- root /var/www/certbot;
- }
-
- # redirect http to https www
- return 301 https://www.bookwyrm.social$request_uri;
-}
-
-server {
- listen [::]:443 ssl http2;
- listen 443 ssl http2;
-
- server_name bookwyrm.social;
-
- # SSL code
- ssl_certificate /etc/nginx/ssl/live/bookwyrm.social/fullchain.pem;
- ssl_certificate_key /etc/nginx/ssl/live/bookwyrm.social/privkey.pem;
-
- location / {
- proxy_pass http://web;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header Host $host;
- proxy_redirect off;
- }
-
- location /images/ {
- alias /app/images/;
- }
-
- location /static/ {
- alias /app/static/;
- }
-}
From ba396f19a643de368c32df50d04ef2bb7faa6c01 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 5 Oct 2020 14:25:53 -0700
Subject: [PATCH 004/416] typos in example domain
---
nginx/default.conf | 10 +++++-----
prod-docker-compose.yml | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/nginx/default.conf b/nginx/default.conf
index 396852e2..51165243 100644
--- a/nginx/default.conf
+++ b/nginx/default.conf
@@ -27,7 +27,7 @@ server {
# listen [::]:80;
# listen 80;
#
-# server_name you-domain.com www.you-domain.com;
+# server_name your-domain.com www.your-domain.com;
#
# location ~ /.well-known/acme-challenge {
# allow all;
@@ -35,18 +35,18 @@ server {
# }
#
# # redirect http to https www
-# return 301 https://www.you-domain.com$request_uri;
+# return 301 https://www.your-domain.com$request_uri;
#}
#
#server {
# listen [::]:443 ssl http2;
# listen 443 ssl http2;
#
-# server_name you-domain.com;
+# server_name your-domain.com;
#
# # SSL code
-# ssl_certificate /etc/nginx/ssl/live/you-domain.com/fullchain.pem;
-# ssl_certificate_key /etc/nginx/ssl/live/you-domain.com/privkey.pem;
+# ssl_certificate /etc/nginx/ssl/live/your-domain.com/fullchain.pem;
+# ssl_certificate_key /etc/nginx/ssl/live/your-domain.com/privkey.pem;
#
# location / {
# proxy_pass http://web;
diff --git a/prod-docker-compose.yml b/prod-docker-compose.yml
index 5f5ac9c6..0ace0df0 100644
--- a/prod-docker-compose.yml
+++ b/prod-docker-compose.yml
@@ -18,7 +18,7 @@ services:
- media_volume:/app/images
certbot:
image: certbot/certbot:latest
- command: certonly --webroot --webroot-path=/var/www/certbot --email mouse.reeve@gmail.com --agree-tos --no-eff-email -d bookwyrm.social -d www.bookwyrm.social
+ command: certonly --webroot --webroot-path=/var/www/certbot --email your-email@domain.com --agree-tos --no-eff-email -d your-domain.com -d www.your-domain.com
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/logs:/var/log/letsencrypt
From d8800b09c4b7e202c8c638591811dc75a8a2176e Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 5 Oct 2020 14:58:57 -0700
Subject: [PATCH 005/416] use remote id for followers links
this should be stored in the db
---
bookwyrm/templates/user_header.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/templates/user_header.html b/bookwyrm/templates/user_header.html
index 6f499e39..6c501c6a 100644
--- a/bookwyrm/templates/user_header.html
+++ b/bookwyrm/templates/user_header.html
@@ -25,8 +25,8 @@
{{ user.username }}
Joined {{ user.created_date | naturaltime }}
- {{ user.followers.count }} follower{{ user.followers.count | pluralize }} ,
- {{ user.following.count }} following
+ {{ user.followers.count }} follower{{ user.followers.count | pluralize }} ,
+ {{ user.following.count }} following
From 51e9977d558d218431c1af121e974425ed5a1aba Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 5 Oct 2020 15:23:39 -0700
Subject: [PATCH 006/416] Received bytes, expecting a string
This doesn't seem like a *good* solution, but I'm not sure why
sometimes this receives strings and sometimes bytes (maybe it's
based on how the data is served).
---
bookwyrm/incoming.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 144400f9..54e2fb24 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -37,7 +37,10 @@ def shared_inbox(request):
return HttpResponseNotFound()
try:
- activity = json.loads(request.body)
+ resp = request.body
+ if isinstance(resp, bytes):
+ resp = json.loads(resp)
+ activity = json.loads(resp)
activity_object = activity['object']
except (json.decoder.JSONDecodeError, KeyError):
return HttpResponseBadRequest()
From 704e1092c42b5905e6cb8631274ed20a5d58b538 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 8 Oct 2020 12:32:45 -0700
Subject: [PATCH 007/416] Delete statuses
---
bookwyrm/activitypub/__init__.py | 1 +
bookwyrm/activitypub/note.py | 8 ++++++++
bookwyrm/models/status.py | 16 +++++++++++++++-
bookwyrm/outgoing.py | 7 +++++++
bookwyrm/status.py | 5 +++++
bookwyrm/templates/snippets/status.html | 11 +++++++++++
bookwyrm/urls.py | 4 +++-
bookwyrm/view_actions.py | 21 +++++++++++++++++++++
8 files changed, 71 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py
index 03c714a6..45cd42a5 100644
--- a/bookwyrm/activitypub/__init__.py
+++ b/bookwyrm/activitypub/__init__.py
@@ -4,6 +4,7 @@ import sys
from .base_activity import ActivityEncoder, Image, PublicKey, Signature
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
+from .note import Tombstone
from .interaction import Boost, Like
from .ordered_collection import OrderedCollection, OrderedCollectionPage
from .person import Person
diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py
index 63ac8a6e..54730fb6 100644
--- a/bookwyrm/activitypub/note.py
+++ b/bookwyrm/activitypub/note.py
@@ -4,6 +4,14 @@ from typing import Dict, List
from .base_activity import ActivityObject, Image
+@dataclass(init=False)
+class Tombstone(ActivityObject):
+ url: str
+ published: str
+ deleted: str
+ type: str = 'Tombstone'
+
+
@dataclass(init=False)
class Note(ActivityObject):
''' Note activity '''
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index 6c3369f2..f9f90467 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -22,6 +22,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
sensitive = models.BooleanField(default=False)
# the created date can't be this, because of receiving federated posts
published_date = models.DateTimeField(default=timezone.now)
+ deleted = models.BooleanField(default=False)
+ deleted_date = models.DateTimeField(default=timezone.now)
favorites = models.ManyToManyField(
'User',
symmetrical=False,
@@ -104,6 +106,18 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
**kwargs
)
+ def to_activity(self, **kwargs):
+ ''' return tombstone if the status is deleted '''
+ if self.deleted:
+ return activitypub.Tombstone(
+ id=self.remote_id,
+ url=self.remote_id,
+ deleted=http_date(self.deleted_date.timestamp()),
+ published=http_date(self.deleted_date.timestamp()),
+ ).serialize()
+ return ActivitypubMixin.to_activity(self, **kwargs)
+
+
class GeneratedStatus(Status):
''' these are app-generated messages about user activity '''
@property
@@ -112,7 +126,7 @@ class GeneratedStatus(Status):
message = self.content
books = ', '.join(
'"%s" ' % (self.book.local_id, self.book.title) \
- for book in self.mention_books
+ for book in self.mention_books.all()
)
return '%s %s' % (message, books)
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index 25a61c46..92187ffa 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -13,6 +13,7 @@ from bookwyrm.status import create_review, create_status
from bookwyrm.status import create_quotation, create_comment
from bookwyrm.status import create_tag, create_notification, create_rating
from bookwyrm.status import create_generated_note
+from bookwyrm.status import delete_status
from bookwyrm.remote_user import get_or_create_remote_user
@@ -197,6 +198,12 @@ def handle_import_books(user, items):
return None
+def handle_delete_status(user, status):
+ ''' delete a status and broadcast deletion to other servers '''
+ delete_status(status)
+ broadcast(user, status.to_activity())
+
+
def handle_rate(user, book, rating):
''' a review that's just a rating '''
builder = create_rating
diff --git a/bookwyrm/status.py b/bookwyrm/status.py
index 0c13638e..190f5dd7 100644
--- a/bookwyrm/status.py
+++ b/bookwyrm/status.py
@@ -6,6 +6,11 @@ from bookwyrm.books_manager import get_or_create_book
from bookwyrm.sanitize_html import InputHtmlParser
+def delete_status(status):
+ ''' replace the status with a tombstone '''
+ status.deleted = True
+ status.save()
+
def create_rating(user, book, rating):
''' a review that's just a rating '''
if not rating or rating < 1 or rating > 5:
diff --git a/bookwyrm/templates/snippets/status.html b/bookwyrm/templates/snippets/status.html
index f1fab742..809cbe9e 100644
--- a/bookwyrm/templates/snippets/status.html
+++ b/bookwyrm/templates/snippets/status.html
@@ -25,6 +25,17 @@
Public post
+ {% if status.user == request.user %}
+
+ {% endif %}
{{ status.published_date | naturaltime }}
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index f1b33877..331efee5 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -11,7 +11,7 @@ localname_regex = r'(?P[\w\-_]+)'
user_path = r'^user/%s' % username_regex
local_user_path = r'^user/%s' % localname_regex
-status_types = ['status', 'review', 'comment', 'quotation', 'boost']
+status_types = ['status', 'review', 'comment', 'quotation', 'boost', 'generatedstatus']
status_path = r'%s/(%s)/(?P\d+)' % \
(local_user_path, '|'.join(status_types))
@@ -107,6 +107,8 @@ urlpatterns = [
re_path(r'^unfavorite/(?P\d+)/?$', actions.unfavorite),
re_path(r'^boost/(?P\d+)/?$', actions.boost),
+ re_path(r'^delete-status/?$', actions.delete_status),
+
re_path(r'^shelve/?$', actions.shelve),
re_path(r'^follow/?$', actions.follow),
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py
index 992a270d..e7674bb9 100644
--- a/bookwyrm/view_actions.py
+++ b/bookwyrm/view_actions.py
@@ -418,6 +418,27 @@ def boost(request, status_id):
outgoing.handle_boost(request.user, status)
return redirect(request.headers.get('Referer', '/'))
+
+@login_required
+def delete_status(request):
+ ''' delete and tombstone a status '''
+ status_id = request.POST.get('status')
+ if not status_id:
+ return HttpResponseBadRequest()
+ try:
+ status = models.Status.objects.get(id=status_id)
+ except models.Status.DoesNotExist:
+ return HttpResponseBadRequest()
+
+ # don't let people delete other people's statuses
+ if status.user != request.user:
+ return HttpResponseBadRequest()
+
+ # perform deletion
+ outgoing.handle_delete_status(request.user, status)
+ return redirect(request.headers.get('Referer', '/'))
+
+
@login_required
def follow(request):
''' follow another user, here or abroad '''
From 48df06aea79a8b2718eec9ff6cb45348510550a9 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 8 Oct 2020 12:35:27 -0700
Subject: [PATCH 008/416] Filter out deleted statuses in feed
---
bookwyrm/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bookwyrm/views.py b/bookwyrm/views.py
index 2bc840c0..f020d287 100644
--- a/bookwyrm/views.py
+++ b/bookwyrm/views.py
@@ -117,7 +117,7 @@ def get_activity_feed(user, filter_level, model=models.Status):
activities = model
if hasattr(model, 'objects'):
- activities = model.objects
+ activities = model.objects.filter(deleted=False)
activities = activities.order_by(
'-created_date'
From 0d614c7ebb394c5233a57223796ce51fb1b23309 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 8 Oct 2020 12:38:06 -0700
Subject: [PATCH 009/416] Don't show deleted statuses
---
bookwyrm/templates/snippets/status.html | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/bookwyrm/templates/snippets/status.html b/bookwyrm/templates/snippets/status.html
index 809cbe9e..5c570e0d 100644
--- a/bookwyrm/templates/snippets/status.html
+++ b/bookwyrm/templates/snippets/status.html
@@ -1,6 +1,7 @@
{% load humanize %}
{% load fr_display %}
+{% if not status.deleted %}
+{% else %}
+
+
+
+{% endif %}
From 10a0a6ac3776fb34015a3235ba61fcea97a37a2d Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 8 Oct 2020 12:40:47 -0700
Subject: [PATCH 010/416] hide deleted statuses from threads
---
bookwyrm/templatetags/fr_display.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py
index 818eae2a..cb4ee419 100644
--- a/bookwyrm/templatetags/fr_display.py
+++ b/bookwyrm/templatetags/fr_display.py
@@ -42,7 +42,8 @@ def get_replies(status):
''' get all direct replies to a status '''
#TODO: this limit could cause problems
return models.Status.objects.filter(
- reply_parent=status
+ reply_parent=status,
+ deleted=False,
).select_subclasses().all()[:10]
From a6d436d05d8299e9f63c188f6abacbf9c2be2095 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Tue, 13 Oct 2020 16:20:04 -0700
Subject: [PATCH 011/416] Fixes avatar in top bar on user page
---
bookwyrm/templates/layout.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html
index f14b76aa..f5f72e65 100644
--- a/bookwyrm/templates/layout.html
+++ b/bookwyrm/templates/layout.html
@@ -60,7 +60,7 @@
{% if request.user.is_authenticated %}
- {% include 'snippets/avatar.html' with user=user %}
+ {% include 'snippets/avatar.html' with user=request.user %}
{% include 'snippets/username.html' with user=request.user %}
From d689b6e7c44fe0eb3733d4071a5b619503077b6f Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 08:38:51 -0700
Subject: [PATCH 012/416] Adds Delete verb
---
bookwyrm/activitypub/__init__.py | 2 +-
bookwyrm/activitypub/verbs.py | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py
index 45cd42a5..446455fa 100644
--- a/bookwyrm/activitypub/__init__.py
+++ b/bookwyrm/activitypub/__init__.py
@@ -9,7 +9,7 @@ from .interaction import Boost, Like
from .ordered_collection import OrderedCollection, OrderedCollectionPage
from .person import Person
from .book import Edition, Work, Author
-from .verbs import Create, Undo, Update
+from .verbs import Create, Delete, Undo, Update
from .verbs import Follow, Accept, Reject
from .verbs import Add, Remove
diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py
index 1ae106b0..bd6d882d 100644
--- a/bookwyrm/activitypub/verbs.py
+++ b/bookwyrm/activitypub/verbs.py
@@ -21,6 +21,15 @@ class Create(Verb):
type: str = 'Create'
+@dataclass(init=False)
+class Delete(Verb):
+ ''' Create activity '''
+ to: List
+ cc: List
+ signature: Signature
+ type: str = 'Delete'
+
+
@dataclass(init=False)
class Update(Verb):
''' Update activity '''
From 4ba9b7a1193323305ecc691a8b53432f5461e8ba Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 09:19:23 -0700
Subject: [PATCH 013/416] Revamps search page
still needs to expand user search to do database lookups
---
bookwyrm/templates/search_results.html | 45 ++++++++++++++++++++++++++
bookwyrm/templates/user_results.html | 1 +
bookwyrm/views.py | 29 +++++++++--------
3 files changed, 62 insertions(+), 13 deletions(-)
create mode 100644 bookwyrm/templates/search_results.html
diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html
new file mode 100644
index 00000000..573dc1ee
--- /dev/null
+++ b/bookwyrm/templates/search_results.html
@@ -0,0 +1,45 @@
+{% extends 'layout.html' %}
+{% block content %}
+
+
+
Matching Books
+ {% for result_set in book_results %}
+ {% if result_set.results %}
+
+ {% if not result_set.connector.local %}
+
+ {% endif %}
+
+ {% for result in result_set.results %}
+
+
+
+ {% endfor %}
+
+ {% endif %}
+ {% endfor %}
+ {% if not results %}
+
No books found for "{{ query }}"
+ {% endif %}
+
+
+
Matching Users
+ {% if not user_results %}
+
No users found for "{{ query }}"
+ {% endif %}
+ {% for result in user_results %}
+
+ {% include 'snippets/avatar.html' with user=result %}
+ {% include 'snippets/username.html' with user=result show_full=True %}
+ {% include 'snippets/follow_button.html' with user=result %}
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/bookwyrm/templates/user_results.html b/bookwyrm/templates/user_results.html
index 9ea169e2..accfdc46 100644
--- a/bookwyrm/templates/user_results.html
+++ b/bookwyrm/templates/user_results.html
@@ -6,6 +6,7 @@
No results found for "{{ query }}"
{% endif %}
{% for result in results %}
+ {{ result }}
{% include 'snippets/avatar.html' with user=result %}
{% include 'snippets/username.html' with user=result show_full=True %}
diff --git a/bookwyrm/views.py b/bookwyrm/views.py
index 2bc840c0..0cc9df18 100644
--- a/bookwyrm/views.py
+++ b/bookwyrm/views.py
@@ -147,22 +147,25 @@ def get_activity_feed(user, filter_level, model=models.Status):
def search(request):
''' that search bar up top '''
query = request.GET.get('q')
- if re.match(r'\w+@\w+.\w+', query):
- # if something looks like a username, search with webfinger
- results = outgoing.handle_account_search(query)
- return TemplateResponse(
- request, 'user_results.html', {'results': results, 'query': query}
- )
-
- # or just send the question over to book search
if is_api_request(request):
- # only return local results via json so we don't cause a cascade
- results = books_manager.local_search(query)
- return JsonResponse([r.__dict__ for r in results], safe=False)
+ # only return local book results via json so we don't cause a cascade
+ book_results = books_manager.local_search(query)
+ return JsonResponse([r.__dict__ for r in book_results], safe=False)
- results = books_manager.search(query)
- return TemplateResponse(request, 'book_results.html', {'results': results})
+ user_results = []
+ # use webfinger looks like a mastodon style account@domain.com username
+ if re.match(r'\w+@\w+.\w+', query):
+ # if something looks like a username, search with webfinger
+ user_results = outgoing.handle_remote_webfinger(query)
+
+ book_results = books_manager.search(query)
+ data = {
+ 'book_results': book_results,
+ 'user_results': user_results,
+ 'query': query,
+ }
+ return TemplateResponse(request, 'search_results.html', data)
@login_required
From 22410e3f479f8f4b61256344f56e4c47171f8e07 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 09:20:36 -0700
Subject: [PATCH 014/416] Adds deleted database fields to Status
---
.../migrations/0053_auto_20201006_2020.py | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 bookwyrm/migrations/0053_auto_20201006_2020.py
diff --git a/bookwyrm/migrations/0053_auto_20201006_2020.py b/bookwyrm/migrations/0053_auto_20201006_2020.py
new file mode 100644
index 00000000..515fc446
--- /dev/null
+++ b/bookwyrm/migrations/0053_auto_20201006_2020.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.7 on 2020-10-06 20:20
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0052_auto_20201005_2145'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='status',
+ name='deleted',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='status',
+ name='deleted_date',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ ]
From b36b306934b2e889a0c37e859f00736cf3181866 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 09:21:53 -0700
Subject: [PATCH 015/416] differentiate local user search and webfinger lookup
---
bookwyrm/outgoing.py | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index 25a61c46..0efa8a4a 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -34,7 +34,7 @@ def outbox(request, username):
)
-def handle_account_search(query):
+def handle_remote_webfinger(query):
''' webfingerin' other servers '''
user = None
domain = query.split('@')[1]
@@ -61,14 +61,10 @@ def handle_account_search(query):
def handle_follow(user, to_follow):
''' someone local wants to follow someone '''
- try:
- relationship, _ = models.UserFollowRequest.objects.get_or_create(
- user_subject=user,
- user_object=to_follow,
- )
- except IntegrityError as err:
- if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
- raise
+ relationship, _ = models.UserFollowRequest.objects.get_or_create(
+ user_subject=user,
+ user_object=to_follow,
+ )
activity = relationship.to_activity()
broadcast(user, activity, direct_recipients=[to_follow])
From fe03e9504954eb3db83365c0663482e468db54e2 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 09:26:46 -0700
Subject: [PATCH 016/416] better bytes checking
but this still seems like the wrong thing
---
bookwyrm/incoming.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 54e2fb24..042a524d 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -38,9 +38,9 @@ def shared_inbox(request):
try:
resp = request.body
- if isinstance(resp, bytes):
- resp = json.loads(resp)
activity = json.loads(resp)
+ if isinstance(resp, str):
+ resp = json.loads(resp)
activity_object = activity['object']
except (json.decoder.JSONDecodeError, KeyError):
return HttpResponseBadRequest()
From aa2e4da6f0a17fef168533801b9babb8f38a4eb5 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 09:54:07 -0700
Subject: [PATCH 017/416] Search local users as well as webfinger
---
bookwyrm/templates/search_results.html | 2 +-
bookwyrm/views.py | 16 +++++++++++++---
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html
index 573dc1ee..bd5096fe 100644
--- a/bookwyrm/templates/search_results.html
+++ b/bookwyrm/templates/search_results.html
@@ -24,7 +24,7 @@
{% endif %}
{% endfor %}
- {% if not results %}
+ {% if not book_results %}
No books found for "{{ query }}"
{% endif %}
diff --git a/bookwyrm/views.py b/bookwyrm/views.py
index 0cc9df18..f659db36 100644
--- a/bookwyrm/views.py
+++ b/bookwyrm/views.py
@@ -2,6 +2,7 @@
import re
from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.postgres.search import SearchRank, SearchVector
from django.db.models import Avg, Count, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
JsonResponse
@@ -153,11 +154,20 @@ def search(request):
book_results = books_manager.local_search(query)
return JsonResponse([r.__dict__ for r in book_results], safe=False)
- user_results = []
# use webfinger looks like a mastodon style account@domain.com username
if re.match(r'\w+@\w+.\w+', query):
- # if something looks like a username, search with webfinger
- user_results = outgoing.handle_remote_webfinger(query)
+ outgoing.handle_remote_webfinger(query)
+
+ # do a local user search
+ vector = SearchVector('localname', weight='A') + \
+ SearchVector('username', wieght='A')
+ user_results = models.User.objects.annotate(
+ search=vector
+ ).annotate(
+ rank=SearchRank(vector, query)
+ ).filter(
+ rank__gt=0
+ ).order_by('-rank')[:10]
book_results = books_manager.search(query)
data = {
From 7a9d6099fcd2c5c64e6846077b45c5e24cab9487 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 10:04:03 -0700
Subject: [PATCH 018/416] Use triagram similarity for user search
---
bookwyrm/migrations/0053_auto_20201014_1700.py | 15 +++++++++++++++
bookwyrm/views.py | 12 ++++--------
2 files changed, 19 insertions(+), 8 deletions(-)
create mode 100644 bookwyrm/migrations/0053_auto_20201014_1700.py
diff --git a/bookwyrm/migrations/0053_auto_20201014_1700.py b/bookwyrm/migrations/0053_auto_20201014_1700.py
new file mode 100644
index 00000000..d1306dfe
--- /dev/null
+++ b/bookwyrm/migrations/0053_auto_20201014_1700.py
@@ -0,0 +1,15 @@
+# Generated by Django 3.0.7 on 2020-10-14 17:00
+
+from django.contrib.postgres.operations import TrigramExtension
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0052_auto_20201005_2145'),
+ ]
+
+ operations = [
+ TrigramExtension()
+ ]
diff --git a/bookwyrm/views.py b/bookwyrm/views.py
index f659db36..9217c4b3 100644
--- a/bookwyrm/views.py
+++ b/bookwyrm/views.py
@@ -2,7 +2,7 @@
import re
from django.contrib.auth.decorators import login_required, permission_required
-from django.contrib.postgres.search import SearchRank, SearchVector
+from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Avg, Count, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
JsonResponse
@@ -159,15 +159,11 @@ def search(request):
outgoing.handle_remote_webfinger(query)
# do a local user search
- vector = SearchVector('localname', weight='A') + \
- SearchVector('username', wieght='A')
user_results = models.User.objects.annotate(
- search=vector
- ).annotate(
- rank=SearchRank(vector, query)
+ similarity=TrigramSimilarity('username', query),
).filter(
- rank__gt=0
- ).order_by('-rank')[:10]
+ similarity__gt=0.1,
+ ).order_by('-similarity')[:10]
book_results = books_manager.search(query)
data = {
From 122418deb8f2226b3059445d6f4012c644de0f12 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 10:24:05 -0700
Subject: [PATCH 019/416] wrong variable in byte check
---
bookwyrm/incoming.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 042a524d..0241fefb 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -39,8 +39,8 @@ def shared_inbox(request):
try:
resp = request.body
activity = json.loads(resp)
- if isinstance(resp, str):
- resp = json.loads(resp)
+ if isinstance(activity, str):
+ activity = json.loads(activity)
activity_object = activity['object']
except (json.decoder.JSONDecodeError, KeyError):
return HttpResponseBadRequest()
From 2ce3cae193a8169466351757b55973b13fcdd1d7 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 10:26:41 -0700
Subject: [PATCH 020/416] Keep user page at local domain
---
bookwyrm/templates/user_header.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/templates/user_header.html b/bookwyrm/templates/user_header.html
index 6c501c6a..ecd1ec2d 100644
--- a/bookwyrm/templates/user_header.html
+++ b/bookwyrm/templates/user_header.html
@@ -25,8 +25,8 @@
{{ user.username }}
Joined {{ user.created_date | naturaltime }}
- {{ user.followers.count }} follower{{ user.followers.count | pluralize }} ,
- {{ user.following.count }} following
+ {{ user.followers.count }} follower{{ user.followers.count | pluralize }} ,
+ {{ user.following.count }} following
From cedc79a962775b8e4fa150f6586e5d8a3ff60f10 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Wed, 14 Oct 2020 17:29:43 -0700
Subject: [PATCH 021/416] Tweaks handle_follow behavior for unknown users
---
bookwyrm/incoming.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 0241fefb..b223ab16 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -112,9 +112,16 @@ def has_valid_signature(request, activity):
def handle_follow(activity):
''' someone wants to follow a local user '''
# figure out who they want to follow -- not using get_or_create because
- # we only allow you to follow local users
- to_follow = models.User.objects.get(remote_id=activity['object'])
- # raises models.User.DoesNotExist id the remote id is not found
+ # we only care if you want to follow local users
+ try:
+ to_follow = models.User.objects.get(remote_id=activity['object'])
+ except models.User.DoesNotExist:
+ # some rando, who cares
+ return
+ if not to_follow.local:
+ # just ignore follow alerts about other servers. maybe they should be
+ # handled. maybe they shouldn't be sent at all.
+ return
# figure out who the actor is
user = get_or_create_remote_user(activity['actor'])
From e8ef8f710180e2cdf10075a23f57193f3dd9d916 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 15 Oct 2020 10:55:04 -0700
Subject: [PATCH 022/416] Fixes data encoding for signing tests
---
bookwyrm/tests/test_signing.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py
index 7373c76f..62870336 100644
--- a/bookwyrm/tests/test_signing.py
+++ b/bookwyrm/tests/test_signing.py
@@ -61,7 +61,7 @@ class Signature(TestCase):
digest=None,
date=None):
now = date or http_date()
- data = get_follow_data(sender, self.rat)
+ data = json.dumps(get_follow_data(sender, self.rat)).encode('utf-8')
digest = digest or make_digest(data)
signature = make_signature(
signer or sender, self.rat.inbox, now, digest)
From db18014325f13205d2ceda58acdd81c936058508 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 15 Oct 2020 17:32:24 -0700
Subject: [PATCH 023/416] Adds test for incoming follow request
---
bookwyrm/incoming.py | 11 +++---
bookwyrm/tests/test_incoming_follow.py | 47 ++++++++++++++++++++++++++
2 files changed, 53 insertions(+), 5 deletions(-)
create mode 100644 bookwyrm/tests/test_incoming_follow.py
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index b223ab16..5aa975b4 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -124,10 +124,10 @@ def handle_follow(activity):
return
# figure out who the actor is
- user = get_or_create_remote_user(activity['actor'])
+ actor = get_or_create_remote_user(activity['actor'])
try:
relationship = models.UserFollowRequest.objects.create(
- user_subject=user,
+ user_subject=actor,
user_object=to_follow,
relationship_id=activity['id']
)
@@ -143,14 +143,15 @@ def handle_follow(activity):
status_builder.create_notification(
to_follow,
'FOLLOW',
- related_user=user
+ related_user=actor
)
- outgoing.handle_accept(user, to_follow, relationship)
+ outgoing.handle_accept(actor, to_follow, relationship)
else:
+ # Accept will be triggered manually
status_builder.create_notification(
to_follow,
'FOLLOW_REQUEST',
- related_user=user
+ related_user=actor
)
diff --git a/bookwyrm/tests/test_incoming_follow.py b/bookwyrm/tests/test_incoming_follow.py
new file mode 100644
index 00000000..a3ac3ebe
--- /dev/null
+++ b/bookwyrm/tests/test_incoming_follow.py
@@ -0,0 +1,47 @@
+import json
+from django.test import TestCase
+
+from bookwyrm import models, incoming
+
+
+class Follow(TestCase):
+ ''' not too much going on in the books model but here we are '''
+ def setUp(self):
+ self.remote_user = models.User.objects.create_user(
+ 'rat', 'rat@rat.com', 'ratword',
+ local=False,
+ remote_id='https://example.com/users/rat',
+ inbox='https://example.com/users/rat/inbox',
+ outbox='https://example.com/users/rat/outbox',
+ )
+ self.local_user = models.User.objects.create_user(
+ 'mouse', 'mouse@mouse.com', 'mouseword')
+ self.local_user.remote_id = 'http://local.com/user/mouse'
+ self.local_user.save()
+
+
+ def test_handle_follow(self):
+ activity = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.com/users/rat/follows/123",
+ "type": "Follow",
+ "actor": "https://example.com/users/rat",
+ "object": "http://local.com/user/mouse"
+ }
+
+ incoming.handle_follow(activity)
+
+ # notification created
+ notification = models.Notification.objects.get()
+ self.assertEqual(notification.user, self.local_user)
+ self.assertEqual(notification.notification_type, 'FOLLOW')
+
+ # the request should have been deleted
+ requests = models.UserFollowRequest.objects.all()
+ self.assertEqual(list(requests), [])
+
+ # the follow relationship should exist
+ follow = models.UserFollows.objects.get(user_object=self.local_user)
+ self.assertEqual(follow.user_subject, self.remote_user)
+
+ # an Accept should be sent out
From 2d2863d4a8b202076077eb57261c7e4cf2c24b8a Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 15 Oct 2020 17:46:23 -0700
Subject: [PATCH 024/416] Adds more incoming follow test cases
---
bookwyrm/tests/test_incoming_follow.py | 49 +++++++++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/bookwyrm/tests/test_incoming_follow.py b/bookwyrm/tests/test_incoming_follow.py
index a3ac3ebe..b5fbec00 100644
--- a/bookwyrm/tests/test_incoming_follow.py
+++ b/bookwyrm/tests/test_incoming_follow.py
@@ -44,4 +44,51 @@ class Follow(TestCase):
follow = models.UserFollows.objects.get(user_object=self.local_user)
self.assertEqual(follow.user_subject, self.remote_user)
- # an Accept should be sent out
+
+ def test_handle_follow_manually_approved(self):
+ activity = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.com/users/rat/follows/123",
+ "type": "Follow",
+ "actor": "https://example.com/users/rat",
+ "object": "http://local.com/user/mouse"
+ }
+
+ self.local_user.manually_approves_followers = True
+ self.local_user.save()
+
+ incoming.handle_follow(activity)
+
+ # notification created
+ notification = models.Notification.objects.get()
+ self.assertEqual(notification.user, self.local_user)
+ self.assertEqual(notification.notification_type, 'FOLLOW_REQUEST')
+
+ # the request should exist
+ request = models.UserFollowRequest.objects.get()
+ self.assertEqual(request.user_subject, self.remote_user)
+ self.assertEqual(request.user_object, self.local_user)
+
+ # the follow relationship should not exist
+ follow = models.UserFollows.objects.all()
+ self.assertEqual(list(follow), [])
+
+
+ def test_nonexistent_user_follow(self):
+ activity = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.com/users/rat/follows/123",
+ "type": "Follow",
+ "actor": "https://example.com/users/rat",
+ "object": "http://local.com/user/nonexistent-user"
+ }
+
+ incoming.handle_follow(activity)
+
+ # do nothing
+ notifications = models.Notification.objects.all()
+ self.assertEqual(list(notifications), [])
+ requests = models.UserFollowRequest.objects.all()
+ self.assertEqual(list(requests), [])
+ follows = models.UserFollows.objects.all()
+ self.assertEqual(list(follows), [])
From 7a01d284c681f16cb3dc825cb6cb16a5b2600764 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 09:23:14 -0700
Subject: [PATCH 025/416] Incoming follow accept test
---
bookwyrm/tests/test_incoming_follow_accept.py | 51 +++++++++++++++++++
1 file changed, 51 insertions(+)
create mode 100644 bookwyrm/tests/test_incoming_follow_accept.py
diff --git a/bookwyrm/tests/test_incoming_follow_accept.py b/bookwyrm/tests/test_incoming_follow_accept.py
new file mode 100644
index 00000000..c30dc248
--- /dev/null
+++ b/bookwyrm/tests/test_incoming_follow_accept.py
@@ -0,0 +1,51 @@
+import json
+from django.test import TestCase
+
+from bookwyrm import models, incoming
+
+
+class IncomingFollowAccept(TestCase):
+ ''' not too much going on in the books model but here we are '''
+ def setUp(self):
+ self.remote_user = models.User.objects.create_user(
+ 'rat', 'rat@rat.com', 'ratword',
+ local=False,
+ remote_id='https://example.com/users/rat',
+ inbox='https://example.com/users/rat/inbox',
+ outbox='https://example.com/users/rat/outbox',
+ )
+ self.local_user = models.User.objects.create_user(
+ 'mouse', 'mouse@mouse.com', 'mouseword')
+ self.local_user.remote_id = 'http://local.com/user/mouse'
+ self.local_user.save()
+
+
+ def test_handle_follow_accept(self):
+ activity = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.com/users/rat/follows/123#accepts",
+ "type": "Accept",
+ "actor": "https://example.com/users/rat",
+ "object": {
+ "id": "https://example.com/users/rat/follows/123",
+ "type": "Follow",
+ "actor": "http://local.com/user/mouse",
+ "object": "https://example.com/users/rat"
+ }
+ }
+
+ models.UserFollowRequest.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user
+ )
+ self.assertEqual(models.UserFollowRequest.objects.count(), 1)
+
+ incoming.handle_follow_accept(activity)
+
+ # request should be deleted
+ self.assertEqual(models.UserFollowRequest.objects.count(), 0)
+
+ # relationship should be created
+ follows = self.remote_user.followers
+ self.assertEqual(follows.count(), 1)
+ self.assertEqual(follows.first(), self.local_user)
From 7a153e185a798f491cb315878393de87ae77df46 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 09:45:14 -0700
Subject: [PATCH 026/416] User activitypub tests
---
bookwyrm/tests/activitypub/test_person.py | 9 ----
bookwyrm/tests/models/test_user_model.py | 51 +++++++++++++++++------
2 files changed, 39 insertions(+), 21 deletions(-)
diff --git a/bookwyrm/tests/activitypub/test_person.py b/bookwyrm/tests/activitypub/test_person.py
index 8a077a29..bec9e19b 100644
--- a/bookwyrm/tests/activitypub/test_person.py
+++ b/bookwyrm/tests/activitypub/test_person.py
@@ -21,12 +21,3 @@ class Person(TestCase):
self.assertEqual(activity.id, 'https://example.com/user/mouse')
self.assertEqual(activity.preferredUsername, 'mouse')
self.assertEqual(activity.type, 'Person')
-
-
- def test_serialize_model(self):
- activity = self.user.to_activity()
- self.assertEqual(activity['id'], self.user.remote_id)
- self.assertEqual(
- activity['endpoints'],
- {'sharedInbox': self.user.shared_inbox}
- )
diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py
index a423de37..0b43cc00 100644
--- a/bookwyrm/tests/models/test_user_model.py
+++ b/bookwyrm/tests/models/test_user_model.py
@@ -7,28 +7,55 @@ from bookwyrm.settings import DOMAIN
class User(TestCase):
def setUp(self):
- models.User.objects.create_user(
+ self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
def test_computed_fields(self):
''' username instead of id here '''
- user = models.User.objects.get(localname='mouse')
expected_id = 'https://%s/user/mouse' % DOMAIN
- self.assertEqual(user.remote_id, expected_id)
- self.assertEqual(user.username, 'mouse@%s' % DOMAIN)
- self.assertEqual(user.localname, 'mouse')
- self.assertEqual(user.shared_inbox, 'https://%s/inbox' % DOMAIN)
- self.assertEqual(user.inbox, '%s/inbox' % expected_id)
- self.assertEqual(user.outbox, '%s/outbox' % expected_id)
- self.assertIsNotNone(user.private_key)
- self.assertIsNotNone(user.public_key)
+ self.assertEqual(self.user.remote_id, expected_id)
+ self.assertEqual(self.user.username, 'mouse@%s' % DOMAIN)
+ self.assertEqual(self.user.localname, 'mouse')
+ self.assertEqual(self.user.shared_inbox, 'https://%s/inbox' % DOMAIN)
+ self.assertEqual(self.user.inbox, '%s/inbox' % expected_id)
+ self.assertEqual(self.user.outbox, '%s/outbox' % expected_id)
+ self.assertIsNotNone(self.user.private_key)
+ self.assertIsNotNone(self.user.public_key)
def test_user_shelves(self):
- user = models.User.objects.get(localname='mouse')
- shelves = models.Shelf.objects.filter(user=user).all()
+ shelves = models.Shelf.objects.filter(user=self.user).all()
self.assertEqual(len(shelves), 3)
names = [s.name for s in shelves]
self.assertEqual(names, ['To Read', 'Currently Reading', 'Read'])
ids = [s.identifier for s in shelves]
self.assertEqual(ids, ['to-read', 'reading', 'read'])
+
+
+ def test_activitypub_serialize(self):
+ activity = self.user.to_activity()
+ self.assertEqual(activity['id'], self.user.remote_id)
+ self.assertEqual(activity['@context'], [
+ 'https://www.w3.org/ns/activitystreams',
+ 'https://w3id.org/security/v1',
+ {
+ 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
+ 'schema': 'http://schema.org#',
+ 'PropertyValue': 'schema:PropertyValue',
+ 'value': 'schema:value',
+ }
+ ])
+ self.assertEqual(activity['preferredUsername'], self.user.localname)
+ self.assertEqual(activity['name'], self.user.name)
+ self.assertEqual(activity['inbox'], self.user.inbox)
+ self.assertEqual(activity['outbox'], self.user.outbox)
+ self.assertEqual(activity['followers'], self.user.ap_followers)
+ self.assertEqual(activity['bookwyrmUser'], False)
+ self.assertEqual(activity['discoverable'], True)
+ self.assertEqual(activity['type'], 'Person')
+
+ def test_activitypub_outbox(self):
+ activity = self.user.to_outbox()
+ self.assertEqual(activity['type'], 'OrderedCollection')
+ self.assertEqual(activity['id'], self.user.outbox)
+ self.assertEqual(activity['totalItems'], 0)
From 2a0af0138dcdf1e9631c37f7a822fa005548e12b Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 10:37:33 -0700
Subject: [PATCH 027/416] Uses activitypub mixin in relationship models
plus tests
---
bookwyrm/incoming.py | 2 +-
.../migrations/0054_auto_20201016_1707.py | 25 ++++
bookwyrm/models/relationship.py | 42 +++---
.../tests/models/test_relationship_models.py | 120 ++++++++++++++++++
4 files changed, 167 insertions(+), 22 deletions(-)
create mode 100644 bookwyrm/migrations/0054_auto_20201016_1707.py
create mode 100644 bookwyrm/tests/models/test_relationship_models.py
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index b223ab16..c9e88dbb 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -129,7 +129,7 @@ def handle_follow(activity):
relationship = models.UserFollowRequest.objects.create(
user_subject=user,
user_object=to_follow,
- relationship_id=activity['id']
+ remote_id=activity['id']
)
except django.db.utils.IntegrityError as err:
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
diff --git a/bookwyrm/migrations/0054_auto_20201016_1707.py b/bookwyrm/migrations/0054_auto_20201016_1707.py
new file mode 100644
index 00000000..043ff12d
--- /dev/null
+++ b/bookwyrm/migrations/0054_auto_20201016_1707.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.0.7 on 2020-10-16 17:07
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0053_auto_20201014_1700'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='userblocks',
+ name='relationship_id',
+ ),
+ migrations.RemoveField(
+ model_name='userfollowrequest',
+ name='relationship_id',
+ ),
+ migrations.RemoveField(
+ model_name='userfollows',
+ name='relationship_id',
+ ),
+ ]
diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py
index e357955e..e86b9098 100644
--- a/bookwyrm/models/relationship.py
+++ b/bookwyrm/models/relationship.py
@@ -2,10 +2,10 @@
from django.db import models
from bookwyrm import activitypub
-from .base_model import BookWyrmModel
+from .base_model import ActivitypubMixin, ActivityMapping, BookWyrmModel
-class UserRelationship(BookWyrmModel):
+class UserRelationship(ActivitypubMixin, BookWyrmModel):
''' many-to-many through table for followers '''
user_subject = models.ForeignKey(
'User',
@@ -17,8 +17,6 @@ class UserRelationship(BookWyrmModel):
on_delete=models.PROTECT,
related_name='%(class)s_user_object'
)
- # follow or follow_request for pending TODO: blocking?
- relationship_id = models.CharField(max_length=100)
class Meta:
''' relationships should be unique '''
@@ -34,25 +32,35 @@ class UserRelationship(BookWyrmModel):
)
]
- def get_remote_id(self):
+ activity_mappings = [
+ ActivityMapping('id', 'remote_id'),
+ ActivityMapping('actor', 'user_subject'),
+ ActivityMapping('object', 'user_object'),
+ ]
+ activity_serializer = activitypub.Follow
+
+ def get_remote_id(self, status=None):
''' use shelf identifier in remote_id '''
+ status = status or 'follows'
base_path = self.user_subject.remote_id
- return '%s#%s/%d' % (base_path, self.status, self.id)
+ return '%s#%s/%d' % (base_path, status, self.id)
+
def to_accept_activity(self):
''' generate an Accept for this follow request '''
return activitypub.Accept(
- id='%s#accepts/follows/' % self.remote_id,
- actor=self.user_subject.remote_id,
- object=self.user_object.remote_id,
+ id=self.get_remote_id(status='accepts'),
+ actor=self.user_object.remote_id,
+ object=self.to_activity()
).serialize()
+
def to_reject_activity(self):
''' generate an Accept for this follow request '''
return activitypub.Reject(
- id='%s#rejects/follows/' % self.remote_id,
- actor=self.user_subject.remote_id,
- object=self.user_object.remote_id,
+ id=self.get_remote_id(status='rejects'),
+ actor=self.user_object.remote_id,
+ object=self.to_activity()
).serialize()
@@ -66,7 +74,7 @@ class UserFollows(UserRelationship):
return cls(
user_subject=follow_request.user_subject,
user_object=follow_request.user_object,
- relationship_id=follow_request.relationship_id,
+ remote_id=follow_request.remote_id,
)
@@ -74,14 +82,6 @@ class UserFollowRequest(UserRelationship):
''' following a user requires manual or automatic confirmation '''
status = 'follow_request'
- def to_activity(self):
- ''' request activity '''
- return activitypub.Follow(
- id=self.remote_id,
- actor=self.user_subject.remote_id,
- object=self.user_object.remote_id,
- ).serialize()
-
class UserBlocks(UserRelationship):
''' prevent another user from following you and seeing your posts '''
diff --git a/bookwyrm/tests/models/test_relationship_models.py b/bookwyrm/tests/models/test_relationship_models.py
new file mode 100644
index 00000000..1e763f59
--- /dev/null
+++ b/bookwyrm/tests/models/test_relationship_models.py
@@ -0,0 +1,120 @@
+''' testing models '''
+from django.test import TestCase
+
+from bookwyrm import models
+
+
+class Relationship(TestCase):
+ def setUp(self):
+ self.remote_user = models.User.objects.create_user(
+ 'rat', 'rat@rat.com', 'ratword',
+ local=False,
+ remote_id='https://example.com/users/rat',
+ inbox='https://example.com/users/rat/inbox',
+ outbox='https://example.com/users/rat/outbox',
+ )
+ self.local_user = models.User.objects.create_user(
+ 'mouse', 'mouse@mouse.com', 'mouseword')
+ self.local_user.remote_id = 'http://local.com/user/mouse'
+ self.local_user.save()
+
+ def test_user_follows(self):
+ rel = models.UserFollows.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user
+ )
+
+ self.assertEqual(
+ rel.remote_id,
+ 'http://local.com/user/mouse#follows/%d' % rel.id
+ )
+
+ activity = rel.to_activity()
+ self.assertEqual(activity['id'], rel.remote_id)
+ self.assertEqual(activity['actor'], self.local_user.remote_id)
+ self.assertEqual(activity['object'], self.remote_user.remote_id)
+
+ def test_user_follow_accept_serialization(self):
+ rel = models.UserFollows.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user
+ )
+
+ self.assertEqual(
+ rel.remote_id,
+ 'http://local.com/user/mouse#follows/%d' % rel.id
+ )
+ accept = rel.to_accept_activity()
+ self.assertEqual(accept['type'], 'Accept')
+ self.assertEqual(
+ accept['id'],
+ 'http://local.com/user/mouse#accepts/%d' % rel.id
+ )
+ self.assertEqual(accept['actor'], self.remote_user.remote_id)
+ self.assertEqual(accept['object']['id'], rel.remote_id)
+ self.assertEqual(accept['object']['actor'], self.local_user.remote_id)
+ self.assertEqual(accept['object']['object'], self.remote_user.remote_id)
+
+ def test_user_follow_reject_serialization(self):
+ rel = models.UserFollows.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user
+ )
+
+ self.assertEqual(
+ rel.remote_id,
+ 'http://local.com/user/mouse#follows/%d' % rel.id
+ )
+ reject = rel.to_reject_activity()
+ self.assertEqual(reject['type'], 'Reject')
+ self.assertEqual(
+ reject['id'],
+ 'http://local.com/user/mouse#rejects/%d' % rel.id
+ )
+ self.assertEqual(reject['actor'], self.remote_user.remote_id)
+ self.assertEqual(reject['object']['id'], rel.remote_id)
+ self.assertEqual(reject['object']['actor'], self.local_user.remote_id)
+ self.assertEqual(reject['object']['object'], self.remote_user.remote_id)
+
+
+ def test_user_follows_from_request(self):
+ request = models.UserFollowRequest.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user
+ )
+ self.assertEqual(
+ request.remote_id,
+ 'http://local.com/user/mouse#follows/%d' % request.id
+ )
+ self.assertEqual(request.status, 'follow_request')
+
+ rel = models.UserFollows.from_request(request)
+ self.assertEqual(
+ rel.remote_id,
+ 'http://local.com/user/mouse#follows/%d' % request.id
+ )
+ self.assertEqual(rel.status, 'follows')
+ self.assertEqual(rel.user_subject, self.local_user)
+ self.assertEqual(rel.user_object, self.remote_user)
+
+
+ def test_user_follows_from_request_custom_remote_id(self):
+ request = models.UserFollowRequest.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user,
+ remote_id='http://antoher.server/sdkfhskdjf/23'
+ )
+ self.assertEqual(
+ request.remote_id,
+ 'http://antoher.server/sdkfhskdjf/23'
+ )
+ self.assertEqual(request.status, 'follow_request')
+
+ rel = models.UserFollows.from_request(request)
+ self.assertEqual(
+ rel.remote_id,
+ 'http://antoher.server/sdkfhskdjf/23'
+ )
+ self.assertEqual(rel.status, 'follows')
+ self.assertEqual(rel.user_subject, self.local_user)
+ self.assertEqual(rel.user_object, self.remote_user)
From b640e6651b34af4fc4c9cfc8ed820d8b659ff077 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 10:52:07 -0700
Subject: [PATCH 028/416] Narrow scope of test coverage reporting
---
fr-dev | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fr-dev b/fr-dev
index ae40e889..56494473 100755
--- a/fr-dev
+++ b/fr-dev
@@ -39,7 +39,7 @@ case "$1" in
;;
test)
shift 1
- docker-compose exec web coverage run --source='.' manage.py test "$@"
+ docker-compose exec web coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@"
;;
test_report)
docker-compose exec web coverage report
From b32fce25d9260eeee443aed37ed68761849ff02f Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 12:24:29 -0700
Subject: [PATCH 029/416] tweaks follow handling
---
bookwyrm/incoming.py | 6 ++----
bookwyrm/outgoing.py | 4 ++--
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 04b7270a..d348e43a 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -134,10 +134,8 @@ def handle_follow(activity):
except django.db.utils.IntegrityError as err:
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
raise
- # Duplicate follow request. Not sure what the correct behaviour is, but
- # just dropping it works for now. We should perhaps generate the
- # Accept, but then do we need to match the activity id?
- return
+ relationship = models.UserFollowRequest.objects.get(remote_id=activity['id'])
+ # send the accept normally for a duplicate request
if not to_follow.manually_approves_followers:
status_builder.create_notification(
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index 0efa8a4a..2c4c6def 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -66,7 +66,7 @@ def handle_follow(user, to_follow):
user_object=to_follow,
)
activity = relationship.to_activity()
- broadcast(user, activity, direct_recipients=[to_follow])
+ broadcast(user, activity, privacy='direct', direct_recipients=[to_follow])
def handle_unfollow(user, to_unfollow):
@@ -76,7 +76,7 @@ def handle_unfollow(user, to_unfollow):
user_object=to_unfollow
)
activity = relationship.to_undo_activity(user)
- broadcast(user, activity, direct_recipients=[to_unfollow])
+ broadcast(user, activity, privacy='direct', direct_recipients=[to_unfollow])
to_unfollow.followers.remove(user)
From b8040cd0dc64d66f3ea32da58ee33612d579a1f3 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 13:02:58 -0700
Subject: [PATCH 030/416] Move prod config files to prod branch
---
docker-compose.yml | 5 +--
nginx/default.conf | 43 ------------------------
prod-docker-compose.yml | 74 -----------------------------------------
3 files changed, 1 insertion(+), 121 deletions(-)
delete mode 100644 prod-docker-compose.yml
diff --git a/docker-compose.yml b/docker-compose.yml
index d7c4ec3b..f5391d42 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,7 +15,6 @@ services:
- media_volume:/app/images
db:
image: postgres
- env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
networks:
@@ -36,14 +35,12 @@ services:
- 8000:8000
redis:
image: redis
- env_file: .env
ports:
- - "6379:6379"
+ - 6379:6379
networks:
- main
restart: on-failure
celery_worker:
- env_file: .env
build: .
networks:
- main
diff --git a/nginx/default.conf b/nginx/default.conf
index 51165243..d3898287 100644
--- a/nginx/default.conf
+++ b/nginx/default.conf
@@ -20,46 +20,3 @@ server {
alias /app/static/;
}
}
-
-# PROD version
-#
-#server {
-# listen [::]:80;
-# listen 80;
-#
-# server_name your-domain.com www.your-domain.com;
-#
-# location ~ /.well-known/acme-challenge {
-# allow all;
-# root /var/www/certbot;
-# }
-#
-# # redirect http to https www
-# return 301 https://www.your-domain.com$request_uri;
-#}
-#
-#server {
-# listen [::]:443 ssl http2;
-# listen 443 ssl http2;
-#
-# server_name your-domain.com;
-#
-# # SSL code
-# ssl_certificate /etc/nginx/ssl/live/your-domain.com/fullchain.pem;
-# ssl_certificate_key /etc/nginx/ssl/live/your-domain.com/privkey.pem;
-#
-# location / {
-# proxy_pass http://web;
-# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-# proxy_set_header Host $host;
-# proxy_redirect off;
-# }
-#
-# location /images/ {
-# alias /app/images/;
-# }
-#
-# location /static/ {
-# alias /app/static/;
-# }
-#}
diff --git a/prod-docker-compose.yml b/prod-docker-compose.yml
deleted file mode 100644
index 0ace0df0..00000000
--- a/prod-docker-compose.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-version: '3'
-
-services:
- nginx:
- image: nginx:latest
- ports:
- - 80:80
- - 443:443
- depends_on:
- - web
- networks:
- - main
- volumes:
- - ./nginx:/etc/nginx/conf.d
- - ./certbot/conf:/etc/nginx/ssl
- - ./certbot/data:/var/www/certbot
- - static_volume:/app/static
- - media_volume:/app/images
- certbot:
- image: certbot/certbot:latest
- command: certonly --webroot --webroot-path=/var/www/certbot --email your-email@domain.com --agree-tos --no-eff-email -d your-domain.com -d www.your-domain.com
- volumes:
- - ./certbot/conf:/etc/letsencrypt
- - ./certbot/logs:/var/log/letsencrypt
- - ./certbot/data:/var/www/certbot
- db:
- image: postgres
- env_file: .env
- volumes:
- - pgdata:/var/lib/postgresql/data
- networks:
- - main
- web:
- build: .
- command: python manage.py runserver 0.0.0.0:8000
- volumes:
- - .:/app
- - static_volume:/app/static
- - media_volume:/app/images
- depends_on:
- - db
- - celery_worker
- networks:
- - main
- ports:
- - 8000:8000
- redis:
- image: redis
- env_file: .env
- ports:
- - "6379:6379"
- networks:
- - main
- restart: on-failure
- celery_worker:
- env_file: .env
- build: .
- networks:
- - main
- command: celery -A celerywyrm worker -l info
- volumes:
- - .:/app
- - static_volume:/app/static
- - media_volume:/app/images
- depends_on:
- - db
- - redis
- restart: on-failure
-volumes:
- pgdata:
- static_volume:
- media_volume:
-networks:
- main:
From cae7bbf834dd67b55f00469fb385ee62afcc1b75 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 13:20:12 -0700
Subject: [PATCH 031/416] oh apparently I DID need to explicitly name .env
---
docker-compose.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docker-compose.yml b/docker-compose.yml
index f5391d42..29ec83ee 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,7 @@ services:
- media_volume:/app/images
db:
image: postgres
+ env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
networks:
@@ -35,12 +36,14 @@ services:
- 8000:8000
redis:
image: redis
+ env_file: .env
ports:
- 6379:6379
networks:
- main
restart: on-failure
celery_worker:
+ env_file: .env
build: .
networks:
- main
From 694de44f3f0eeb0860d3c1706a73ff11efb1ca6e Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 14:04:06 -0700
Subject: [PATCH 032/416] reorganize incoming/outgoing tests
---
bookwyrm/tests/incoming/__init__.py | 1 +
.../test_favorite.py} | 3 +-
.../test_follow.py} | 4 +-
.../test_follow_accept.py} | 2 -
bookwyrm/tests/outgoing/__init__.py | 1 +
bookwyrm/tests/outgoing/test_follow.py | 37 +++++++++++++++++++
6 files changed, 41 insertions(+), 7 deletions(-)
create mode 100644 bookwyrm/tests/incoming/__init__.py
rename bookwyrm/tests/{test_incoming_favorite.py => incoming/test_favorite.py} (95%)
rename bookwyrm/tests/{test_incoming_follow.py => incoming/test_follow.py} (96%)
rename bookwyrm/tests/{test_incoming_follow_accept.py => incoming/test_follow_accept.py} (95%)
create mode 100644 bookwyrm/tests/outgoing/__init__.py
create mode 100644 bookwyrm/tests/outgoing/test_follow.py
diff --git a/bookwyrm/tests/incoming/__init__.py b/bookwyrm/tests/incoming/__init__.py
new file mode 100644
index 00000000..b6e690fd
--- /dev/null
+++ b/bookwyrm/tests/incoming/__init__.py
@@ -0,0 +1 @@
+from . import *
diff --git a/bookwyrm/tests/test_incoming_favorite.py b/bookwyrm/tests/incoming/test_favorite.py
similarity index 95%
rename from bookwyrm/tests/test_incoming_favorite.py
rename to bookwyrm/tests/incoming/test_favorite.py
index 03502145..eeba9000 100644
--- a/bookwyrm/tests/test_incoming_favorite.py
+++ b/bookwyrm/tests/incoming/test_favorite.py
@@ -6,7 +6,6 @@ from bookwyrm import models, incoming
class Favorite(TestCase):
- ''' not too much going on in the books model but here we are '''
def setUp(self):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
@@ -25,7 +24,7 @@ class Favorite(TestCase):
)
datafile = pathlib.Path(__file__).parent.joinpath(
- 'data/ap_user.json'
+ '../data/ap_user.json'
)
self.user_data = json.loads(datafile.read_bytes())
diff --git a/bookwyrm/tests/test_incoming_follow.py b/bookwyrm/tests/incoming/test_follow.py
similarity index 96%
rename from bookwyrm/tests/test_incoming_follow.py
rename to bookwyrm/tests/incoming/test_follow.py
index b5fbec00..51ab3c43 100644
--- a/bookwyrm/tests/test_incoming_follow.py
+++ b/bookwyrm/tests/incoming/test_follow.py
@@ -1,11 +1,9 @@
-import json
from django.test import TestCase
from bookwyrm import models, incoming
-class Follow(TestCase):
- ''' not too much going on in the books model but here we are '''
+class IncomingFollow(TestCase):
def setUp(self):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
diff --git a/bookwyrm/tests/test_incoming_follow_accept.py b/bookwyrm/tests/incoming/test_follow_accept.py
similarity index 95%
rename from bookwyrm/tests/test_incoming_follow_accept.py
rename to bookwyrm/tests/incoming/test_follow_accept.py
index c30dc248..ba88bb40 100644
--- a/bookwyrm/tests/test_incoming_follow_accept.py
+++ b/bookwyrm/tests/incoming/test_follow_accept.py
@@ -1,11 +1,9 @@
-import json
from django.test import TestCase
from bookwyrm import models, incoming
class IncomingFollowAccept(TestCase):
- ''' not too much going on in the books model but here we are '''
def setUp(self):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
diff --git a/bookwyrm/tests/outgoing/__init__.py b/bookwyrm/tests/outgoing/__init__.py
new file mode 100644
index 00000000..b6e690fd
--- /dev/null
+++ b/bookwyrm/tests/outgoing/__init__.py
@@ -0,0 +1 @@
+from . import *
diff --git a/bookwyrm/tests/outgoing/test_follow.py b/bookwyrm/tests/outgoing/test_follow.py
new file mode 100644
index 00000000..7932d9c1
--- /dev/null
+++ b/bookwyrm/tests/outgoing/test_follow.py
@@ -0,0 +1,37 @@
+from django.test import TestCase
+
+from bookwyrm import models, outgoing
+
+
+class OutgoingFollow(TestCase):
+ def setUp(self):
+ self.remote_user = models.User.objects.create_user(
+ 'rat', 'rat@rat.com', 'ratword',
+ local=False,
+ remote_id='https://example.com/users/rat',
+ inbox='https://example.com/users/rat/inbox',
+ outbox='https://example.com/users/rat/outbox',
+ )
+ self.local_user = models.User.objects.create_user(
+ 'mouse', 'mouse@mouse.com', 'mouseword',
+ local=True,
+ remote_id='http://local.com/users/mouse',
+ )
+
+
+ def test_handle_follow(self):
+ self.assertEqual(models.UserFollowRequest.objects.count(), 0)
+
+ outgoing.handle_follow(self.local_user, self.remote_user)
+ rel = models.UserFollowRequest.objects.get()
+
+ self.assertEqual(rel.user_subject, self.local_user)
+ self.assertEqual(rel.user_object, self.remote_user)
+ self.assertEqual(rel.status, 'follow_request')
+
+ def test_handle_unfollow(self):
+ self.remote_user.followers.add(self.local_user)
+ self.assertEqual(self.remote_user.followers.count(), 1)
+ outgoing.handle_unfollow(self.local_user, self.remote_user)
+
+ self.assertEqual(self.remote_user.followers.count(), 0)
From a567bd4e613c6aa7d8c692ba1e271c9207cc0126 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 14:14:07 -0700
Subject: [PATCH 033/416] Simplifies outgoing follow logic
---
bookwyrm/incoming.py | 2 +-
bookwyrm/outgoing.py | 4 +++-
bookwyrm/tests/outgoing/test_follow.py | 19 ++++++++++++++++++-
bookwyrm/view_actions.py | 2 +-
4 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index d348e43a..57ed0220 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -143,7 +143,7 @@ def handle_follow(activity):
'FOLLOW',
related_user=actor
)
- outgoing.handle_accept(actor, to_follow, relationship)
+ outgoing.handle_accept(relationship)
else:
# Accept will be triggered manually
status_builder.create_notification(
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index 2c4c6def..0a09a101 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -80,8 +80,10 @@ def handle_unfollow(user, to_unfollow):
to_unfollow.followers.remove(user)
-def handle_accept(user, to_follow, follow_request):
+def handle_accept(follow_request):
''' send an acceptance message to a follow request '''
+ user = follow_request.user_subject
+ to_follow = follow_request.user_object
with transaction.atomic():
relationship = models.UserFollows.from_request(follow_request)
follow_request.delete()
diff --git a/bookwyrm/tests/outgoing/test_follow.py b/bookwyrm/tests/outgoing/test_follow.py
index 7932d9c1..4ecf3a91 100644
--- a/bookwyrm/tests/outgoing/test_follow.py
+++ b/bookwyrm/tests/outgoing/test_follow.py
@@ -3,7 +3,7 @@ from django.test import TestCase
from bookwyrm import models, outgoing
-class OutgoingFollow(TestCase):
+class Following(TestCase):
def setUp(self):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
@@ -29,9 +29,26 @@ class OutgoingFollow(TestCase):
self.assertEqual(rel.user_object, self.remote_user)
self.assertEqual(rel.status, 'follow_request')
+
def test_handle_unfollow(self):
self.remote_user.followers.add(self.local_user)
self.assertEqual(self.remote_user.followers.count(), 1)
outgoing.handle_unfollow(self.local_user, self.remote_user)
self.assertEqual(self.remote_user.followers.count(), 0)
+
+
+ def test_handle_accept(self):
+ rel = models.UserFollowRequest.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user
+ )
+ rel_id = rel.id
+
+ outgoing.handle_accept(rel)
+ # request should be deleted
+ self.assertEqual(
+ models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
+ )
+ # follow relationship should exist
+ self.assertEqual(self.remote_user.followers.first(), self.local_user)
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py
index 992a270d..a4814ff0 100644
--- a/bookwyrm/view_actions.py
+++ b/bookwyrm/view_actions.py
@@ -473,7 +473,7 @@ def accept_follow_request(request):
# Request already dealt with.
pass
else:
- outgoing.handle_accept(requester, request.user, follow_request)
+ outgoing.handle_accept(follow_request)
return redirect('/user/%s' % request.user.localname)
From 75c695b3c60e305bbd7b9fd58c1e70f592b1b83d Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 14:28:25 -0700
Subject: [PATCH 034/416] Updates and tests outgoing reject
---
bookwyrm/incoming.py | 6 ++++--
bookwyrm/outgoing.py | 8 +++++---
bookwyrm/tests/outgoing/test_follow.py | 18 ++++++++++++++++++
bookwyrm/view_actions.py | 2 +-
4 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 57ed0220..bbce14c1 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -134,7 +134,9 @@ def handle_follow(activity):
except django.db.utils.IntegrityError as err:
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
raise
- relationship = models.UserFollowRequest.objects.get(remote_id=activity['id'])
+ relationship = models.UserFollowRequest.objects.get(
+ remote_id=activity['id']
+ )
# send the accept normally for a duplicate request
if not to_follow.manually_approves_followers:
@@ -194,7 +196,7 @@ def handle_follow_reject(activity):
user_object=rejecter
)
request.delete()
- #raises models.UserFollowRequest.DoesNotExist:
+ #raises models.UserFollowRequest.DoesNotExist
@app.task
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index 0a09a101..8775712a 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -93,10 +93,12 @@ def handle_accept(follow_request):
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
-def handle_reject(user, to_follow, relationship):
+def handle_reject(follow_request):
''' a local user who managed follows rejects a follow request '''
- activity = relationship.to_reject_activity(user)
- relationship.delete()
+ user = follow_request.user_subject
+ to_follow = follow_request.user_object
+ activity = follow_request.to_reject_activity()
+ follow_request.delete()
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
diff --git a/bookwyrm/tests/outgoing/test_follow.py b/bookwyrm/tests/outgoing/test_follow.py
index 4ecf3a91..82a476f6 100644
--- a/bookwyrm/tests/outgoing/test_follow.py
+++ b/bookwyrm/tests/outgoing/test_follow.py
@@ -52,3 +52,21 @@ class Following(TestCase):
)
# follow relationship should exist
self.assertEqual(self.remote_user.followers.first(), self.local_user)
+
+
+ def test_handle_reject(self):
+ rel = models.UserFollowRequest.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user
+ )
+ rel_id = rel.id
+
+ outgoing.handle_reject(rel)
+ # request should be deleted
+ self.assertEqual(
+ models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
+ )
+ # follow relationship should not exist
+ self.assertEqual(
+ models.UserFollows.objects.filter(id=rel_id).count(), 0
+ )
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py
index a4814ff0..b8436157 100644
--- a/bookwyrm/view_actions.py
+++ b/bookwyrm/view_actions.py
@@ -495,7 +495,7 @@ def delete_follow_request(request):
except models.UserFollowRequest.DoesNotExist:
return HttpResponseBadRequest()
- outgoing.handle_reject(requester, request.user, follow_request)
+ outgoing.handle_reject(follow_request)
return redirect('/user/%s' % request.user.localname)
From 4f07a567bd5386c0eac9c32cacd0330d1cbaa2a5 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 15:07:41 -0700
Subject: [PATCH 035/416] Shelving tests
---
bookwyrm/outgoing.py | 15 ++--
bookwyrm/tests/outgoing/test_shelving.py | 97 ++++++++++++++++++++++++
bookwyrm/tests/status/test_quotation.py | 4 +-
3 files changed, 108 insertions(+), 8 deletions(-)
create mode 100644 bookwyrm/tests/outgoing/test_shelving.py
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index 8775712a..2db667d7 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -111,11 +111,16 @@ def handle_shelve(user, book, shelf):
broadcast(user, shelve.to_add_activity(user))
# tell the world about this cool thing that happened
- message = {
- 'to-read': 'wants to read',
- 'reading': 'started reading',
- 'read': 'finished reading'
- }[shelf.identifier]
+ try:
+ message = {
+ 'to-read': 'wants to read',
+ 'reading': 'started reading',
+ 'read': 'finished reading'
+ }[shelf.identifier]
+ except KeyError:
+ # it's a non-standard shelf, don't worry about it
+ return
+
status = create_generated_note(user, message, mention_books=[book])
status.save()
diff --git a/bookwyrm/tests/outgoing/test_shelving.py b/bookwyrm/tests/outgoing/test_shelving.py
new file mode 100644
index 00000000..acf816e1
--- /dev/null
+++ b/bookwyrm/tests/outgoing/test_shelving.py
@@ -0,0 +1,97 @@
+from django.test import TestCase
+
+from bookwyrm import models, outgoing
+
+
+class Shelving(TestCase):
+ def setUp(self):
+ self.user = models.User.objects.create_user(
+ 'mouse', 'mouse@mouse.com', 'mouseword',
+ local=True,
+ remote_id='http://local.com/users/mouse',
+ )
+ self.book = models.Edition.objects.create(
+ title='Example Edition',
+ remote_id='https://example.com/book/1',
+ )
+ self.shelf = models.Shelf.objects.create(
+ name='Test Shelf',
+ identifier='test-shelf',
+ user=self.user
+ )
+
+
+ def test_handle_shelve(self):
+ outgoing.handle_shelve(self.user, self.book, self.shelf)
+ # make sure the book is on the shelf
+ self.assertEqual(self.shelf.books.get(), self.book)
+
+
+ def test_handle_shelve_to_read(self):
+ shelf = models.Shelf.objects.get(identifier='to-read')
+
+ outgoing.handle_shelve(self.user, self.book, shelf)
+ # make sure the book is on the shelf
+ self.assertEqual(shelf.books.get(), self.book)
+
+ # it should have posted a status about this
+ status = models.GeneratedStatus.objects.get()
+ self.assertEqual(status.content, 'wants to read')
+ self.assertEqual(status.user, self.user)
+ self.assertEqual(status.mention_books.count(), 1)
+ self.assertEqual(status.mention_books.first(), self.book)
+
+ # and it should not create a read-through
+ self.assertEqual(models.ReadThrough.objects.count(), 0)
+
+
+ def test_handle_shelve_reading(self):
+ shelf = models.Shelf.objects.get(identifier='reading')
+
+ outgoing.handle_shelve(self.user, self.book, shelf)
+ # make sure the book is on the shelf
+ self.assertEqual(shelf.books.get(), self.book)
+
+ # it should have posted a status about this
+ status = models.GeneratedStatus.objects.order_by('-published_date').first()
+ self.assertEqual(status.content, 'started reading')
+ self.assertEqual(status.user, self.user)
+ self.assertEqual(status.mention_books.count(), 1)
+ self.assertEqual(status.mention_books.first(), self.book)
+
+ # and it should create a read-through
+ readthrough = models.ReadThrough.objects.get()
+ self.assertEqual(readthrough.user, self.user)
+ self.assertEqual(readthrough.book.id, self.book.id)
+ self.assertIsNotNone(readthrough.start_date)
+ self.assertIsNone(readthrough.finish_date)
+
+
+ def test_handle_shelve_read(self):
+ shelf = models.Shelf.objects.get(identifier='read')
+
+ outgoing.handle_shelve(self.user, self.book, shelf)
+ # make sure the book is on the shelf
+ self.assertEqual(shelf.books.get(), self.book)
+
+ # it should have posted a status about this
+ status = models.GeneratedStatus.objects.order_by('-published_date').first()
+ self.assertEqual(status.content, 'finished reading')
+ self.assertEqual(status.user, self.user)
+ self.assertEqual(status.mention_books.count(), 1)
+ self.assertEqual(status.mention_books.first(), self.book)
+
+ # and it should update the existing read-through
+ readthrough = models.ReadThrough.objects.get()
+ self.assertEqual(readthrough.user, self.user)
+ self.assertEqual(readthrough.book.id, self.book.id)
+ self.assertIsNotNone(readthrough.start_date)
+ self.assertIsNotNone(readthrough.finish_date)
+
+
+ def test_handle_unshelve(self):
+ self.shelf.books.add(self.book)
+ self.shelf.save()
+ self.assertEqual(self.shelf.books.count(), 1)
+ outgoing.handle_unshelve(self.user, self.book, self.shelf)
+ self.assertEqual(self.shelf.books.count(), 0)
diff --git a/bookwyrm/tests/status/test_quotation.py b/bookwyrm/tests/status/test_quotation.py
index 57755560..4892e21d 100644
--- a/bookwyrm/tests/status/test_quotation.py
+++ b/bookwyrm/tests/status/test_quotation.py
@@ -1,8 +1,6 @@
from django.test import TestCase
-import json
-import pathlib
-from bookwyrm import activitypub, models
+from bookwyrm import models
from bookwyrm import status as status_builder
From c0f51fa6aa45720a6b8da3d50d5466ccf6ef89f6 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 15:40:23 -0700
Subject: [PATCH 036/416] Handle incomign update user activities
---
bookwyrm/incoming.py | 16 ++++++++++-
bookwyrm/tests/incoming/test_update_user.py | 30 +++++++++++++++++++++
2 files changed, 45 insertions(+), 1 deletion(-)
create mode 100644 bookwyrm/tests/incoming/test_update_user.py
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index bbce14c1..dca5d12f 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -67,7 +67,7 @@ def shared_inbox(request):
'Like': handle_unfavorite,
},
'Update': {
- 'Person': None,# TODO: handle_update_user
+ 'Person': handle_update_user,
'Document': handle_update_book,
},
}
@@ -293,6 +293,20 @@ def handle_tag(activity):
status_builder.create_tag(user, book, activity['object']['name'])
+@app.task
+def handle_update_user(activity):
+ ''' receive an updated user Person activity object '''
+ try:
+ user = models.User.objects.get(remote_id=activity['object']['id'])
+ except models.User.DoesNotExist:
+ # who is this person? who cares
+ return
+ activitypub.Person(
+ **activity['object']
+ ).to_model(models.User, instance=user)
+ # model save() happens in the to_model function
+
+
@app.task
def handle_update_book(activity):
''' a remote instance changed a book (Document) '''
diff --git a/bookwyrm/tests/incoming/test_update_user.py b/bookwyrm/tests/incoming/test_update_user.py
new file mode 100644
index 00000000..703078f1
--- /dev/null
+++ b/bookwyrm/tests/incoming/test_update_user.py
@@ -0,0 +1,30 @@
+import json
+import pathlib
+from django.test import TestCase
+
+from bookwyrm import models, incoming
+
+
+class UpdateUser(TestCase):
+ def setUp(self):
+ self.user = models.User.objects.create_user(
+ 'mouse', 'mouse@mouse.com', 'mouseword',
+ remote_id='https://example.com/user/mouse',
+ local=False,
+ localname='mouse'
+ )
+
+ datafile = pathlib.Path(__file__).parent.joinpath(
+ '../data/ap_user.json'
+ )
+ self.user_data = json.loads(datafile.read_bytes())
+
+ def test_handle_update_user(self):
+ self.assertIsNone(self.user.name)
+ self.assertEqual(self.user.localname, 'mouse')
+
+ incoming.handle_update_user({'object': self.user_data})
+ self.user = models.User.objects.get(id=self.user.id)
+
+ self.assertEqual(self.user.name, 'MOUSE?? MOUSE!!')
+ self.assertEqual(self.user.localname, 'mouse')
From 7f579ffefa94e38968788042c5e2df376d9d7787 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 17:00:10 -0700
Subject: [PATCH 037/416] Read incoming deletion activities
---
bookwyrm/incoming.py | 15 +++++++++++++++
bookwyrm/migrations/0054_auto_20201016_2359.py | 18 ++++++++++++++++++
bookwyrm/models/status.py | 2 +-
bookwyrm/status.py | 2 ++
4 files changed, 36 insertions(+), 1 deletion(-)
create mode 100644 bookwyrm/migrations/0054_auto_20201016_2359.py
diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 54e2fb24..d5cfc36b 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -57,6 +57,7 @@ def shared_inbox(request):
'Accept': handle_follow_accept,
'Reject': handle_follow_reject,
'Create': handle_create,
+ 'Delete': handle_delete_status,
'Like': handle_favorite,
'Announce': handle_boost,
'Add': {
@@ -229,6 +230,20 @@ def handle_create(activity):
)
+@app.task
+def handle_delete_status(activity):
+ ''' remove a status '''
+ status_id = activity['object']['id']
+ try:
+ status = models.Status.objects.select_subclasses().get(
+ remote_id=status_id
+ )
+ except models.Status.DoesNotExist:
+ return
+ status_builder.delete_status(status)
+
+
+
@app.task
def handle_favorite(activity):
''' approval of your good good post '''
diff --git a/bookwyrm/migrations/0054_auto_20201016_2359.py b/bookwyrm/migrations/0054_auto_20201016_2359.py
new file mode 100644
index 00000000..c8ab3480
--- /dev/null
+++ b/bookwyrm/migrations/0054_auto_20201016_2359.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-10-16 23:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0053_auto_20201006_2020'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='status',
+ name='deleted_date',
+ field=models.DateTimeField(),
+ ),
+ ]
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index f9f90467..0a70eb77 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -23,7 +23,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
# the created date can't be this, because of receiving federated posts
published_date = models.DateTimeField(default=timezone.now)
deleted = models.BooleanField(default=False)
- deleted_date = models.DateTimeField(default=timezone.now)
+ deleted_date = models.DateTimeField()
favorites = models.ManyToManyField(
'User',
symmetrical=False,
diff --git a/bookwyrm/status.py b/bookwyrm/status.py
index 190f5dd7..25619839 100644
--- a/bookwyrm/status.py
+++ b/bookwyrm/status.py
@@ -1,4 +1,5 @@
''' Handle user activity '''
+from datetime import datetime
from django.db import IntegrityError
from bookwyrm import models
@@ -9,6 +10,7 @@ from bookwyrm.sanitize_html import InputHtmlParser
def delete_status(status):
''' replace the status with a tombstone '''
status.deleted = True
+ status.deleted_date = datetime.now()
status.save()
def create_rating(user, book, rating):
From d1d339225cf35f2bf7bccf3d29383663bfb38632 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 17:11:17 -0700
Subject: [PATCH 038/416] Merge migrations
---
bookwyrm/migrations/0055_merge_20201017_0011.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 bookwyrm/migrations/0055_merge_20201017_0011.py
diff --git a/bookwyrm/migrations/0055_merge_20201017_0011.py b/bookwyrm/migrations/0055_merge_20201017_0011.py
new file mode 100644
index 00000000..fd995eaa
--- /dev/null
+++ b/bookwyrm/migrations/0055_merge_20201017_0011.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.7 on 2020-10-17 00:11
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0054_auto_20201016_1707'),
+ ('bookwyrm', '0054_auto_20201016_2359'),
+ ]
+
+ operations = [
+ ]
From 8cf7e4405d90f78bd9a4c8c2cc4233b610b27d66 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Fri, 16 Oct 2020 19:13:18 -0700
Subject: [PATCH 039/416] minor style fixes
---
bookwyrm/activitypub/base_activity.py | 9 ++++-----
bookwyrm/activitypub/book.py | 1 -
bookwyrm/activitypub/note.py | 2 +-
bookwyrm/broadcast.py | 3 ---
bookwyrm/emailing.py | 1 -
bookwyrm/urls.py | 9 ++++++++-
6 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index 042b8a14..1cffd0ed 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -44,10 +44,9 @@ class ActivityObject:
type: str
def __init__(self, **kwargs):
- ''' this lets you pass in an object with fields
- that aren't in the dataclass, which it ignores.
- Any field in the dataclass is required or has a
- default value '''
+ ''' this lets you pass in an object with fields that aren't in the
+ dataclass, which it ignores. Any field in the dataclass is required or
+ has a default value '''
for field in fields(self):
try:
value = kwargs[field.name]
@@ -59,7 +58,7 @@ class ActivityObject:
def to_model(self, model, instance=None):
- ''' convert from an activity to a model '''
+ ''' convert from an activity to a model instance '''
if not isinstance(self, model.activity_serializer):
raise TypeError('Wrong activity type for model')
diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py
index 2a50dd6a..49f2deb8 100644
--- a/bookwyrm/activitypub/book.py
+++ b/bookwyrm/activitypub/book.py
@@ -52,7 +52,6 @@ class Work(Book):
type: str = 'Work'
-
@dataclass(init=False)
class Author(ActivityObject):
''' author of a book '''
diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py
index 54730fb6..4e36b01f 100644
--- a/bookwyrm/activitypub/note.py
+++ b/bookwyrm/activitypub/note.py
@@ -6,6 +6,7 @@ from .base_activity import ActivityObject, Image
@dataclass(init=False)
class Tombstone(ActivityObject):
+ ''' the placeholder for a deleted status '''
url: str
published: str
deleted: str
@@ -23,7 +24,6 @@ class Note(ActivityObject):
cc: List[str]
content: str
replies: Dict
- # TODO: this is wrong???
attachment: List[Image] = field(default=lambda: [])
sensitive: bool = False
type: str = 'Note'
diff --git a/bookwyrm/broadcast.py b/bookwyrm/broadcast.py
index 301fe84f..9b071b39 100644
--- a/bookwyrm/broadcast.py
+++ b/bookwyrm/broadcast.py
@@ -13,7 +13,6 @@ def get_public_recipients(user, software=None):
''' everybody and their public inboxes '''
followers = user.followers.filter(local=False)
if software:
- # TODO: eventually we may want to handle particular software differently
followers = followers.filter(bookwyrm_user=(software == 'bookwyrm'))
# we want shared inboxes when available
@@ -36,7 +35,6 @@ def broadcast(sender, activity, software=None, \
# start with parsing the direct recipients
recipients = [u.inbox for u in direct_recipients or []]
# and then add any other recipients
- # TODO: other kinds of privacy
if privacy == 'public':
recipients += get_public_recipients(sender, software=software)
broadcast_task.delay(
@@ -55,7 +53,6 @@ def broadcast_task(sender_id, activity, recipients):
try:
sign_and_send(sender, activity, recipient)
except requests.exceptions.HTTPError as e:
- # TODO: maybe keep track of users who cause errors
errors.append({
'error': str(e),
'recipient': recipient,
diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py
index 12dee65f..2319d467 100644
--- a/bookwyrm/emailing.py
+++ b/bookwyrm/emailing.py
@@ -6,7 +6,6 @@ from bookwyrm.tasks import app
def password_reset_email(reset_code):
''' generate a password reset email '''
- # TODO; this should be tempalted
site = models.SiteSettings.get()
send_email.delay(
reset_code.user.email,
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index 331efee5..5d75f49b 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -11,7 +11,14 @@ localname_regex = r'(?P[\w\-_]+)'
user_path = r'^user/%s' % username_regex
local_user_path = r'^user/%s' % localname_regex
-status_types = ['status', 'review', 'comment', 'quotation', 'boost', 'generatedstatus']
+status_types = [
+ 'status',
+ 'review',
+ 'comment',
+ 'quotation',
+ 'boost',
+ 'generatedstatus'
+]
status_path = r'%s/(%s)/(?P\d+)' % \
(local_user_path, '|'.join(status_types))
From 1cc0c14f8627812701b06a8935f6154c4a38c364 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Tue, 20 Oct 2020 18:50:39 -0700
Subject: [PATCH 040/416] Deleted date should be null-able
Fixes #240
---
bookwyrm/migrations/0056_auto_20201021_0150.py | 18 ++++++++++++++++++
bookwyrm/models/status.py | 2 +-
2 files changed, 19 insertions(+), 1 deletion(-)
create mode 100644 bookwyrm/migrations/0056_auto_20201021_0150.py
diff --git a/bookwyrm/migrations/0056_auto_20201021_0150.py b/bookwyrm/migrations/0056_auto_20201021_0150.py
new file mode 100644
index 00000000..1408efb9
--- /dev/null
+++ b/bookwyrm/migrations/0056_auto_20201021_0150.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-10-21 01:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0055_merge_20201017_0011'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='status',
+ name='deleted_date',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ ]
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index 0a70eb77..61e9c500 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -23,7 +23,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
# the created date can't be this, because of receiving federated posts
published_date = models.DateTimeField(default=timezone.now)
deleted = models.BooleanField(default=False)
- deleted_date = models.DateTimeField()
+ deleted_date = models.DateTimeField(blank=True, null=True)
favorites = models.ManyToManyField(
'User',
symmetrical=False,
From 6243cf0e4a763aacb5586338fbf6ef1127aa5349 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 26 Oct 2020 14:33:02 -0700
Subject: [PATCH 041/416] uses enum for post privacy database field
---
bookwyrm/migrations/0057_auto_20201026_2131.py | 18 ++++++++++++++++++
bookwyrm/models/status.py | 13 ++++++++++++-
2 files changed, 30 insertions(+), 1 deletion(-)
create mode 100644 bookwyrm/migrations/0057_auto_20201026_2131.py
diff --git a/bookwyrm/migrations/0057_auto_20201026_2131.py b/bookwyrm/migrations/0057_auto_20201026_2131.py
new file mode 100644
index 00000000..cc414e98
--- /dev/null
+++ b/bookwyrm/migrations/0057_auto_20201026_2131.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-10-26 21:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0056_auto_20201021_0150'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='status',
+ name='privacy',
+ field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
+ ),
+ ]
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index 61e9c500..dfc39194 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -10,6 +10,13 @@ from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from .base_model import ActivityMapping, BookWyrmModel
+PrivacyLevels = models.TextChoices('Privacy', [
+ 'public',
+ 'unlisted',
+ 'followers',
+ 'direct'
+])
+
class Status(OrderedCollectionPageMixin, BookWyrmModel):
''' any post, like a reply to a review, etc '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
@@ -18,7 +25,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
mention_books = models.ManyToManyField(
'Edition', related_name='mention_book')
local = models.BooleanField(default=True)
- privacy = models.CharField(max_length=255, default='public')
+ privacy = models.CharField(
+ max_length=255,
+ default='public',
+ choices=PrivacyLevels.choices
+ )
sensitive = models.BooleanField(default=False)
# the created date can't be this, because of receiving federated posts
published_date = models.DateTimeField(default=timezone.now)
From 2afa111b705eb319e3822e70d9c2f3ce0776d289 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 26 Oct 2020 15:00:15 -0700
Subject: [PATCH 042/416] Create statuses from django form
---
bookwyrm/outgoing.py | 59 +++++++++++-----------------------------
bookwyrm/status.py | 1 +
bookwyrm/view_actions.py | 48 ++++++--------------------------
3 files changed, 26 insertions(+), 82 deletions(-)
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index f496a211..118f90fe 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -9,9 +9,8 @@ import requests
from bookwyrm import activitypub
from bookwyrm import models
from bookwyrm.broadcast import broadcast
-from bookwyrm.status import create_review, create_status
-from bookwyrm.status import create_quotation, create_comment
-from bookwyrm.status import create_tag, create_notification, create_rating
+from bookwyrm.status import create_status
+from bookwyrm.status import create_tag, create_notification
from bookwyrm.status import create_generated_note
from bookwyrm.status import delete_status
from bookwyrm.remote_user import get_or_create_remote_user
@@ -178,16 +177,18 @@ def handle_import_books(user, items):
broadcast(user, activity)
if item.rating or item.review:
- review_title = "Review of {!r} on Goodreads".format(
- item.book.title,
- ) if item.review else ""
- handle_review(
- user,
- item.book,
- review_title,
- item.review,
- item.rating,
- )
+ pass
+ #review_title = "Review of {!r} on Goodreads".format(
+ # item.book.title,
+ #) if item.review else ""
+ # TODO
+ #handle_review(
+ # user,
+ # item.book,
+ # review_title,
+ # item.review,
+ # item.rating,
+ #)
for read in item.reads:
read.book = item.book
read.user = user
@@ -209,37 +210,9 @@ def handle_delete_status(user, status):
broadcast(user, status.to_activity())
-def handle_rate(user, book, rating):
- ''' a review that's just a rating '''
- builder = create_rating
- handle_status(user, book, builder, rating)
-
-
-def handle_review(user, book, name, content, rating):
- ''' post a review '''
- # validated and saves the review in the database so it has an id
- builder = create_review
- handle_status(user, book, builder, name, content, rating)
-
-
-def handle_quotation(user, book, content, quote):
- ''' post a review '''
- # validated and saves the review in the database so it has an id
- builder = create_quotation
- handle_status(user, book, builder, content, quote)
-
-
-def handle_comment(user, book, content):
- ''' post a comment '''
- # validated and saves the review in the database so it has an id
- builder = create_comment
- handle_status(user, book, builder, content)
-
-
-def handle_status(user, book_id, builder, *args):
+def handle_status(user, form):
''' generic handler for statuses '''
- book = models.Edition.objects.get(id=book_id)
- status = builder(user, book, *args)
+ status = form.save()
broadcast(user, status.to_create_activity(user), software='bookwyrm')
diff --git a/bookwyrm/status.py b/bookwyrm/status.py
index 25619839..57c800e5 100644
--- a/bookwyrm/status.py
+++ b/bookwyrm/status.py
@@ -13,6 +13,7 @@ def delete_status(status):
status.deleted_date = datetime.now()
status.save()
+
def create_rating(user, book, rating):
''' a review that's just a rating '''
if not rating or rating < 1 or rating > 5:
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py
index 54ed353a..e8a3b1fc 100644
--- a/bookwyrm/view_actions.py
+++ b/bookwyrm/view_actions.py
@@ -296,67 +296,37 @@ def shelve(request):
def rate(request):
''' just a star rating for a book '''
form = forms.RatingForm(request.POST)
- book_id = request.POST.get('book')
- # TODO: better failure behavior
- if not form.is_valid():
- return redirect('/book/%s' % book_id)
-
- rating = form.cleaned_data.get('rating')
- # throws a value error if the book is not found
-
- outgoing.handle_rate(request.user, book_id, rating)
- return redirect('/book/%s' % book_id)
+ return handle_status(request, form)
@login_required
def review(request):
''' create a book review '''
form = forms.ReviewForm(request.POST)
- book_id = request.POST.get('book')
- if not form.is_valid():
- return redirect('/book/%s' % book_id)
-
- # TODO: validation, htmlification
- name = form.cleaned_data.get('name')
- content = form.cleaned_data.get('content')
- rating = form.data.get('rating', None)
- try:
- rating = int(rating)
- except ValueError:
- rating = None
-
- outgoing.handle_review(request.user, book_id, name, content, rating)
- return redirect('/book/%s' % book_id)
+ return handle_status(request, form)
@login_required
def quotate(request):
''' create a book quotation '''
form = forms.QuotationForm(request.POST)
- book_id = request.POST.get('book')
- if not form.is_valid():
- return redirect('/book/%s' % book_id)
-
- quote = form.cleaned_data.get('quote')
- content = form.cleaned_data.get('content')
-
- outgoing.handle_quotation(request.user, book_id, content, quote)
- return redirect('/book/%s' % book_id)
+ return handle_status(request, form)
@login_required
def comment(request):
''' create a book comment '''
form = forms.CommentForm(request.POST)
+ return handle_status(request, form)
+
+
+def handle_status(request, form):
+ ''' all the review/comment/quote etc functions are the same '''
book_id = request.POST.get('book')
- # TODO: better failure behavior
if not form.is_valid():
return redirect('/book/%s' % book_id)
- # TODO: validation, htmlification
- content = form.data.get('content')
-
- outgoing.handle_comment(request.user, book_id, content)
+ outgoing.handle_status(request.user, form)
return redirect('/book/%s' % book_id)
From 53891443185aa6f3008adf98df2c243c89122ed7 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 26 Oct 2020 15:09:51 -0700
Subject: [PATCH 043/416] Fixes login validation form
---
bookwyrm/templates/login.html | 3 ---
bookwyrm/view_actions.py | 9 +--------
2 files changed, 1 insertion(+), 11 deletions(-)
diff --git a/bookwyrm/templates/login.html b/bookwyrm/templates/login.html
index cbfd2b66..4e50c444 100644
--- a/bookwyrm/templates/login.html
+++ b/bookwyrm/templates/login.html
@@ -15,9 +15,6 @@
{{ login_form.username }}
- {% for error in login_form.username.errors %}
- {{ error | escape }}
- {% endfor %}
Password:
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py
index 54ed353a..3d01e9ff 100644
--- a/bookwyrm/view_actions.py
+++ b/bookwyrm/view_actions.py
@@ -24,14 +24,6 @@ def user_login(request):
return redirect('/login')
login_form = forms.LoginForm(request.POST)
- register_form = forms.RegisterForm()
- if not login_form.is_valid():
- data = {
- 'site_settings': models.SiteSettings.get(),
- 'login_form': login_form,
- 'register_form': register_form
- }
- return TemplateResponse(request, 'login.html', data)
username = login_form.data['username']
username = '%s@%s' % (username, DOMAIN)
@@ -42,6 +34,7 @@ def user_login(request):
return redirect(request.GET.get('next', '/'))
login_form.non_field_errors = 'Username or password are incorrect'
+ register_form = forms.RegisterForm()
data = {
'site_settings': models.SiteSettings.get(),
'login_form': login_form,
From 39b9fe8f4a671bad786e757ea85fd5757184bdc2 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Mon, 26 Oct 2020 15:10:32 -0700
Subject: [PATCH 044/416] Fixes serializing reviews with no rating
---
bookwyrm/models/status.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index dfc39194..b1283c9d 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -192,9 +192,14 @@ class Review(Status):
@property
def ap_pure_name(self):
''' clarify review names for mastodon serialization '''
- return 'Review of "%s" (%d stars): %s' % (
+ if self.rating:
+ return 'Review of "%s" (%d stars): %s' % (
+ self.book.title,
+ self.rating,
+ self.name
+ )
+ return 'Review of "%s": %s' % (
self.book.title,
- self.rating,
self.name
)
From b7061c0f4d13576729a568d647be62e22ab40d4b Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Tue, 27 Oct 2020 11:32:15 -0700
Subject: [PATCH 045/416] Fixes create status forms
---
bookwyrm/forms.py | 26 +++-------------
bookwyrm/outgoing.py | 31 +++++++------------
.../templates/snippets/create_status.html | 8 +++++
bookwyrm/templates/snippets/interaction.html | 4 ++-
bookwyrm/view_actions.py | 26 ++++++----------
5 files changed, 38 insertions(+), 57 deletions(-)
diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py
index 7b18a2ff..0c3b19ba 100644
--- a/bookwyrm/forms.py
+++ b/bookwyrm/forms.py
@@ -52,47 +52,31 @@ class RegisterForm(CustomForm):
class RatingForm(CustomForm):
class Meta:
model = models.Review
- fields = ['rating']
+ fields = ['user', 'book', 'content', 'rating', 'privacy']
class ReviewForm(CustomForm):
class Meta:
model = models.Review
- fields = ['name', 'content']
- help_texts = {f: None for f in fields}
- labels = {
- 'name': 'Title',
- 'content': 'Review',
- }
+ fields = ['user', 'book', 'name', 'content', 'rating', 'privacy']
class CommentForm(CustomForm):
class Meta:
model = models.Comment
- fields = ['content']
- help_texts = {f: None for f in fields}
- labels = {
- 'content': 'Comment',
- }
+ fields = ['user', 'book', 'content', 'privacy']
class QuotationForm(CustomForm):
class Meta:
model = models.Quotation
- fields = ['quote', 'content']
- help_texts = {f: None for f in fields}
- labels = {
- 'quote': 'Quote',
- 'content': 'Comment',
- }
+ fields = ['user', 'book', 'quote', 'content', 'privacy']
class ReplyForm(CustomForm):
class Meta:
model = models.Status
- fields = ['content']
- help_texts = {f: None for f in fields}
- labels = {'content': 'Comment'}
+ fields = ['user', 'content', 'reply_parent', 'privacy']
class EditUserForm(CustomForm):
diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py
index 118f90fe..bcdf7170 100644
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -9,7 +9,6 @@ import requests
from bookwyrm import activitypub
from bookwyrm import models
from bookwyrm.broadcast import broadcast
-from bookwyrm.status import create_status
from bookwyrm.status import create_tag, create_notification
from bookwyrm.status import create_generated_note
from bookwyrm.status import delete_status
@@ -214,12 +213,21 @@ def handle_status(user, form):
''' generic handler for statuses '''
status = form.save()
+ # notify reply parent or (TODO) tagged users
+ if status.reply_parent and status.reply_parent.user.local:
+ create_notification(
+ status.reply_parent.user,
+ 'REPLY',
+ related_user=user,
+ related_status=status
+ )
+
broadcast(user, status.to_create_activity(user), software='bookwyrm')
# re-format the activity for non-bookwyrm servers
- remote_activity = status.to_create_activity(user, pure=True)
-
- broadcast(user, remote_activity, software='other')
+ if hasattr(status, 'pure_activity_serializer'):
+ remote_activity = status.to_create_activity(user, pure=True)
+ broadcast(user, remote_activity, software='other')
def handle_tag(user, book, name):
@@ -238,21 +246,6 @@ def handle_untag(user, book, name):
broadcast(user, tag_activity)
-def handle_reply(user, review, content):
- ''' respond to a review or status '''
- # validated and saves the comment in the database so it has an id
- reply = create_status(user, content, reply_parent=review)
- if reply.reply_parent:
- create_notification(
- reply.reply_parent.user,
- 'REPLY',
- related_user=user,
- related_status=reply,
- )
-
- broadcast(user, reply.to_create_activity(user))
-
-
def handle_favorite(user, status):
''' a user likes a status '''
try:
diff --git a/bookwyrm/templates/snippets/create_status.html b/bookwyrm/templates/snippets/create_status.html
index 88558df9..28eded97 100644
--- a/bookwyrm/templates/snippets/create_status.html
+++ b/bookwyrm/templates/snippets/create_status.html
@@ -22,6 +22,8 @@
diff --git a/bookwyrm/templates/snippets/interaction.html b/bookwyrm/templates/snippets/interaction.html
index 27bbbf8b..a48d8a7c 100644
--- a/bookwyrm/templates/snippets/interaction.html
+++ b/bookwyrm/templates/snippets/interaction.html
@@ -6,7 +6,7 @@
{% csrf_token %}
-
+