From 704e1092c42b5905e6cb8631274ed20a5d58b538 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Oct 2020 12:32:45 -0700 Subject: [PATCH 01/13] 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 03c714a6b..45cd42a5c 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 63ac8a6e0..54730fb67 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 6c3369f21..f9f904672 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 25a61c46e..92187ffac 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 0c13638e2..190f5dd7f 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 f1fab7427..809cbe9e5 100644 --- a/bookwyrm/templates/snippets/status.html +++ b/bookwyrm/templates/snippets/status.html @@ -25,6 +25,17 @@ Public post + {% if status.user == request.user %} +
+ {% csrf_token %} + + +
+ {% endif %} {{ status.published_date | naturaltime }} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index f1b33877a..331efee5f 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 992a270dd..e7674bb91 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 02/13] 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 2bc840c0c..f020d287c 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 03/13] 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 809cbe9e5..5c570e0d6 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 %}
{% include 'snippets/status_header.html' with status=status %} @@ -40,3 +41,14 @@
+{% else %} +
+
+

+ {% include 'snippets/avatar.html' with user=status.user %} + {% include 'snippets/username.html' with user=status.user %} + deleted this status +

+
+
+{% endif %} From 10a0a6ac3776fb34015a3235ba61fcea97a37a2d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Oct 2020 12:40:47 -0700 Subject: [PATCH 04/13] 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 818eae2af..cb4ee4198 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 d689b6e7c44fe0eb3733d4071a5b619503077b6f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 14 Oct 2020 08:38:51 -0700 Subject: [PATCH 05/13] 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 45cd42a5c..446455fa3 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 1ae106b0f..bd6d882d3 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 22410e3f479f8f4b61256344f56e4c47171f8e07 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 14 Oct 2020 09:20:36 -0700 Subject: [PATCH 06/13] 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 000000000..515fc4463 --- /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 b8040cd0dc64d66f3ea32da58ee33612d579a1f3 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 13:02:58 -0700 Subject: [PATCH 07/13] 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 d7c4ec3b9..f5391d421 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 51165243b..d38982870 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 0ace0df0a..000000000 --- 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 08/13] 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 f5391d421..29ec83ee1 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 09/13] 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 000000000..b6e690fd5 --- /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 035021452..eeba9000a 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 b5fbec008..51ab3c43a 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 c30dc2486..ba88bb40d 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 000000000..b6e690fd5 --- /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 000000000..7932d9c17 --- /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 10/13] 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 d348e43a7..57ed0220c 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 2c4c6def9..0a09a101b 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 7932d9c17..4ecf3a915 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 992a270dd..a4814ff01 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 11/13] 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 57ed0220c..bbce14c11 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 0a09a101b..8775712a8 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 4ecf3a915..82a476f62 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 a4814ff01..b8436157c 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 12/13] 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 8775712a8..2db667d70 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 000000000..acf816e11 --- /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 57755560a..4892e21d3 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 7f579ffefa94e38968788042c5e2df376d9d7787 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 17:00:10 -0700 Subject: [PATCH 13/13] 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 54e2fb24a..d5cfc36b2 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 000000000..c8ab34801 --- /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 f9f904672..0a70eb77c 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 190f5dd7f..256198399 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):