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