From aacf5b7ba41d7ac5196aaecf1d8cab3cd5f9c398 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 12 Dec 2020 18:00:39 -0800 Subject: [PATCH 1/8] fields for content warnings --- bookwyrm/activitypub/note.py | 1 + bookwyrm/models/status.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index df28bf8d..263ddb2a 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -23,6 +23,7 @@ class Note(ActivityObject): cc: List[str] = field(default_factory=lambda: []) replies: Dict = field(default_factory=lambda: {}) inReplyTo: str = '' + summary: str = '' tag: List[Link] = field(default_factory=lambda: []) attachment: List[Image] = field(default_factory=lambda: []) sensitive: bool = False diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 55036f2c..308a3c8c 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -23,6 +23,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): default='public', choices=PrivacyLevels.choices ) + content_warning = fields.CharField( + max_length=150, blank=True, null=True, activitypub_field='summary') sensitive = fields.BooleanField(default=False) # the created date can't be this, because of receiving federated posts published_date = fields.DateTimeField( @@ -68,7 +70,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): **kwargs ) - def to_activity(self, pure=False): + def to_activity(self, pure=False):# pylint: disable=arguments-differ ''' return tombstone if the status is deleted ''' if self.deleted: return activitypub.Tombstone( From f1926ce76d79d5a9917f7e75ca7c4352e9af4176 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 16 Dec 2020 16:20:40 -0800 Subject: [PATCH 2/8] Avoid duplicate notifitions And render html --- bookwyrm/incoming.py | 5 ++++- bookwyrm/templates/notifications.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 556c34a2..78e7b970 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -218,7 +218,9 @@ def handle_create(activity): status = activity.to_model(model) # create a notification if this is a reply + notified = [] if status.reply_parent and status.reply_parent.user.local: + notified.append(status.reply_parent.user) status_builder.create_notification( status.reply_parent.user, 'REPLY', @@ -226,7 +228,8 @@ def handle_create(activity): related_status=status, ) if status.mention_users.exists(): - for mentioned_user in status.mention_users.all(): + for mentioned_user in status.mention_users.all() and \ + mentioned_user not in notified: if not mentioned_user.local: continue status_builder.create_notification( diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index f31df76d..ddcbc0fd 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -54,7 +54,7 @@
{{ notification.related_status.published_date | post_date }} From a3c7d324d67fe29202e76fb332b7c328e61faf3c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 16 Dec 2020 16:47:05 -0800 Subject: [PATCH 3/8] Sanitize incoming html --- .../migrations/0025_auto_20201217_0046.py | 39 +++++++++++++++++++ bookwyrm/models/author.py | 2 +- bookwyrm/models/book.py | 2 +- bookwyrm/models/fields.py | 10 +++++ bookwyrm/models/status.py | 4 +- bookwyrm/models/user.py | 2 +- bookwyrm/sanitize_html.py | 2 +- bookwyrm/tests/test_sanitize_html.py | 12 +++--- 8 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 bookwyrm/migrations/0025_auto_20201217_0046.py diff --git a/bookwyrm/migrations/0025_auto_20201217_0046.py b/bookwyrm/migrations/0025_auto_20201217_0046.py new file mode 100644 index 00000000..a3ffe8c1 --- /dev/null +++ b/bookwyrm/migrations/0025_auto_20201217_0046.py @@ -0,0 +1,39 @@ +# Generated by Django 3.0.7 on 2020-12-17 00:46 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0024_merge_20201216_1721'), + ] + + operations = [ + migrations.AlterField( + model_name='author', + name='bio', + field=bookwyrm.models.fields.HtmlField(blank=True, null=True), + ), + migrations.AlterField( + model_name='book', + name='description', + field=bookwyrm.models.fields.HtmlField(blank=True, null=True), + ), + migrations.AlterField( + model_name='quotation', + name='quote', + field=bookwyrm.models.fields.HtmlField(), + ), + migrations.AlterField( + model_name='status', + name='content', + field=bookwyrm.models.fields.HtmlField(blank=True, null=True), + ), + migrations.AlterField( + model_name='user', + name='summary', + field=bookwyrm.models.fields.HtmlField(default=''), + ), + ] diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index 331d2dd6..47714d4e 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -25,7 +25,7 @@ class Author(ActivitypubMixin, BookWyrmModel): aliases = fields.ArrayField( models.CharField(max_length=255), blank=True, default=list ) - bio = fields.TextField(null=True, blank=True) + bio = fields.HtmlField(null=True, blank=True) def save(self, *args, **kwargs): ''' can't be abstract for query reasons, but you shouldn't USE it ''' diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index bcd4bc04..3080a115 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -36,7 +36,7 @@ class Book(ActivitypubMixin, BookWyrmModel): title = fields.CharField(max_length=255) sort_title = fields.CharField(max_length=255, blank=True, null=True) subtitle = fields.CharField(max_length=255, blank=True, null=True) - description = fields.TextField(blank=True, null=True) + description = fields.HtmlField(blank=True, null=True) languages = fields.ArrayField( models.CharField(max_length=255), blank=True, default=list ) diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 5e12f5d5..52933715 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -12,6 +12,7 @@ from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from bookwyrm import activitypub +from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.settings import DOMAIN from bookwyrm.connectors import get_image @@ -362,6 +363,15 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): except (ParserError, TypeError): return None +class HtmlField(ActivitypubFieldMixin, models.TextField): + ''' a text field for storing html ''' + def field_from_activity(self, value): + if not value or value == MISSING: + return None + sanitizer = InputHtmlParser() + sanitizer.feed(value) + return sanitizer.get_output() + class ArrayField(ActivitypubFieldMixin, DjangoArrayField): ''' activitypub-aware array field ''' def field_to_activity(self, value): diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index fcf4a290..66114e7c 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -14,7 +14,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ''' any post, like a reply to a review, etc ''' user = fields.ForeignKey( 'User', on_delete=models.PROTECT, activitypub_field='attributedTo') - content = fields.TextField(blank=True, null=True) + content = fields.HtmlField(blank=True, null=True) mention_users = fields.TagField('User', related_name='mention_user') mention_books = fields.TagField('Edition', related_name='mention_book') local = models.BooleanField(default=True) @@ -134,7 +134,7 @@ class Comment(Status): class Quotation(Status): ''' like a review but without a rating and transient ''' - quote = fields.TextField() + quote = fields.HtmlField() book = fields.ForeignKey( 'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook') diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 63549d36..9d66eb5c 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -42,7 +42,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): blank=True, ) outbox = fields.RemoteIdField(unique=True) - summary = fields.TextField(default='') + summary = fields.HtmlField(default='') local = models.BooleanField(default=False) bookwyrm_user = fields.BooleanField(default=True) localname = models.CharField( diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py index 9c5ca73a..933fc43c 100644 --- a/bookwyrm/sanitize_html.py +++ b/bookwyrm/sanitize_html.py @@ -1,7 +1,7 @@ ''' html parser to clean up incoming text from unknown sources ''' from html.parser import HTMLParser -class InputHtmlParser(HTMLParser): +class InputHtmlParser(HTMLParser):#pylint: disable=abstract-method ''' Removes any html that isn't allowed_tagsed from a block ''' def __init__(self): diff --git a/bookwyrm/tests/test_sanitize_html.py b/bookwyrm/tests/test_sanitize_html.py index 3344a934..58d94311 100644 --- a/bookwyrm/tests/test_sanitize_html.py +++ b/bookwyrm/tests/test_sanitize_html.py @@ -1,34 +1,36 @@ +''' make sure only valid html gets to the app ''' from django.test import TestCase from bookwyrm.sanitize_html import InputHtmlParser - class Sanitizer(TestCase): + ''' sanitizer tests ''' def test_no_html(self): + ''' just text ''' input_text = 'no html ' parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() self.assertEqual(input_text, output) - def test_valid_html(self): + ''' leave the html untouched ''' input_text = 'yes html' parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() self.assertEqual(input_text, output) - def test_valid_html_attrs(self): + ''' and don't remove attributes ''' input_text = 'yes html' parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() self.assertEqual(input_text, output) - def test_invalid_html(self): + ''' remove all html when the html is malformed ''' input_text = 'yes html' parser = InputHtmlParser() parser.feed(input_text) @@ -41,8 +43,8 @@ class Sanitizer(TestCase): output = parser.get_output() self.assertEqual('yes html ', output) - def test_disallowed_html(self): + ''' remove disallowed html but keep allowed html ''' input_text = '
yes html
' parser = InputHtmlParser() parser.feed(input_text) From 42167af3e9dad8aa040b74f66f4b50c0ea98a77b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 16 Dec 2020 18:39:18 -0800 Subject: [PATCH 4/8] Tests fro html field --- bookwyrm/tests/models/test_fields.py | 63 +++++++++++++++++++++------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py index 8c86b23c..81b1d528 100644 --- a/bookwyrm/tests/models/test_fields.py +++ b/bookwyrm/tests/models/test_fields.py @@ -21,31 +21,23 @@ from bookwyrm.activitypub.base_activity import ActivityObject from bookwyrm.models import fields, User, Status from bookwyrm.models.base_model import ActivitypubMixin, BookWyrmModel +#pylint: disable=too-many-public-methods class ActivitypubFields(TestCase): ''' overwrites standard model feilds to work with activitypub ''' def test_validate_remote_id(self): ''' should look like a url ''' - self.assertIsNone(fields.validate_remote_id( - 'http://www.example.com' - )) - self.assertIsNone(fields.validate_remote_id( - 'https://www.example.com' - )) - self.assertIsNone(fields.validate_remote_id( - 'http://example.com/dlfjg-23/x' - )) + self.assertIsNone(fields.validate_remote_id('http://www.example.com')) + self.assertIsNone(fields.validate_remote_id('https://www.example.com')) + self.assertIsNone(fields.validate_remote_id('http://exle.com/dlg-23/x')) self.assertRaises( ValidationError, fields.validate_remote_id, - 'http:/example.com/dlfjg-23/x' - ) + 'http:/example.com/dlfjg-23/x') self.assertRaises( ValidationError, fields.validate_remote_id, - 'www.example.com/dlfjg-23/x' - ) + 'www.example.com/dlfjg-23/x') self.assertRaises( ValidationError, fields.validate_remote_id, - 'http://www.example.com/dlfjg 23/x' - ) + 'http://www.example.com/dlfjg 23/x') def test_activitypub_field_mixin(self): ''' generic mixin with super basic to and from functionality ''' @@ -71,6 +63,38 @@ class ActivitypubFields(TestCase): instance.name = 'snake_case_name' self.assertEqual(instance.get_activitypub_field(), 'snakeCaseName') + def test_set_field_from_activity(self): + ''' setter from entire json blob ''' + @dataclass + class TestModel: + ''' real simple mock ''' + field_name: str + + mock_model = TestModel(field_name='bip') + TestActivity = namedtuple('test', ('fieldName', 'unrelated')) + data = TestActivity(fieldName='hi', unrelated='bfkjh') + + instance = fields.ActivitypubFieldMixin() + instance.name = 'field_name' + + instance.set_field_from_activity(mock_model, data) + self.assertEqual(mock_model.field_name, 'hi') + + def test_set_activity_from_field(self): + ''' set json field given entire model ''' + @dataclass + class TestModel: + ''' real simple mock ''' + field_name: str + unrelated: str + mock_model = TestModel(field_name='bip', unrelated='field') + instance = fields.ActivitypubFieldMixin() + instance.name = 'field_name' + + data = {} + instance.set_activity_from_field(data, mock_model) + self.assertEqual(data['fieldName'], 'bip') + def test_remote_id_field(self): ''' just sets some defaults on charfield ''' instance = fields.RemoteIdField() @@ -408,3 +432,12 @@ class ActivitypubFields(TestCase): ''' idk why it makes them strings but probably for a good reason ''' instance = fields.ArrayField(fields.IntegerField) self.assertEqual(instance.field_to_activity([0, 1]), ['0', '1']) + + + def test_html_field(self): + ''' sanitizes html, the sanitizer has its own tests ''' + instance = fields.HtmlField() + self.assertEqual( + instance.field_from_activity('

hi

'), + '

hi

' + ) From f7cb52598144aa896e8d9518dbb67b03b1b8b70c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 16 Dec 2020 18:40:43 -0800 Subject: [PATCH 5/8] Fixes logic error --- bookwyrm/incoming.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 78e7b970..c1c15ca9 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -228,9 +228,8 @@ def handle_create(activity): related_status=status, ) if status.mention_users.exists(): - for mentioned_user in status.mention_users.all() and \ - mentioned_user not in notified: - if not mentioned_user.local: + for mentioned_user in status.mention_users.all(): + if not mentioned_user.local or mentioned_user in notified: continue status_builder.create_notification( mentioned_user, From b796686483e26011a22954673d143128b150e3b9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 16 Dec 2020 19:20:15 -0800 Subject: [PATCH 6/8] Adds cw field --- .../migrations/0026_status_content_warning.py | 19 +++++++++++++++++++ bookwyrm/models/status.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/migrations/0026_status_content_warning.py diff --git a/bookwyrm/migrations/0026_status_content_warning.py b/bookwyrm/migrations/0026_status_content_warning.py new file mode 100644 index 00000000..f4e494db --- /dev/null +++ b/bookwyrm/migrations/0026_status_content_warning.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-12-17 03:17 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0025_auto_20201217_0046'), + ] + + operations = [ + migrations.AddField( + model_name='status', + name='content_warning', + field=bookwyrm.models.fields.CharField(blank=True, max_length=500, null=True), + ), + ] diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 18b9431e..49fbad55 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -19,7 +19,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): mention_books = fields.TagField('Edition', related_name='mention_book') local = models.BooleanField(default=True) content_warning = fields.CharField( - max_length=150, blank=True, null=True, activitypub_field='summary') + max_length=500, blank=True, null=True, activitypub_field='summary') privacy = fields.PrivacyField(max_length=255) sensitive = fields.BooleanField(default=False) # created date is different than publish date because of federated posts From 0d42b9cf8f765e2b6338b064f4d213a4130d2793 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 16 Dec 2020 19:50:36 -0800 Subject: [PATCH 7/8] Display status cw's --- .../templates/snippets/status_content.html | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/bookwyrm/templates/snippets/status_content.html b/bookwyrm/templates/snippets/status_content.html index 2ef06421..ff87b3b6 100644 --- a/bookwyrm/templates/snippets/status_content.html +++ b/bookwyrm/templates/snippets/status_content.html @@ -1,38 +1,55 @@ {% load bookwyrm_tags %}
- {% if status.status_type == 'Review' %} -

- {% if status.name %}{{ status.name }}
{% endif %} - {% include 'snippets/stars.html' with rating=status.rating %} -

- {% endif %} - - {% if status.quote %} -
-
{{ status.quote }}
- -

— {% include 'snippets/book_titleby.html' with book=status.book %}

-
- {% endif %} - - {% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Boost' %} - {% include 'snippets/trimmed_text.html' with full=status.content|safe %} - {% endif %} - {% if status.attachments %} -
-
- {% for attachment in status.attachments.all %} -
-
- - {{ attachment.caption }} - -
-
- {% endfor %} + {% if status.content_warning %} +
+

{{ status.content_warning }}

+ +
+ + {% endif %} +
{% if not hide_book %} From 172c36b6416c05997e18ce0b75a6ed143bf34ae2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 16 Dec 2020 20:10:50 -0800 Subject: [PATCH 8/8] Adds content warning field in status forms --- bookwyrm/activitypub/note.py | 2 +- bookwyrm/forms.py | 12 ++++++++---- .../templates/snippets/create_status_form.html | 10 ++++++++++ bookwyrm/templates/snippets/reply_form.html | 8 ++++++++ bookwyrm/templates/snippets/status_content.html | 16 +++++++++------- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 263ddb2a..b478c96d 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -54,7 +54,7 @@ class Comment(Note): class Review(Comment): ''' a full book review ''' name: str - rating: int + rating: int = None type: str = 'Review' diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index a2c3e24b..454836bb 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -60,25 +60,29 @@ class RatingForm(CustomForm): class ReviewForm(CustomForm): class Meta: model = models.Review - fields = ['user', 'book', 'name', 'content', 'rating', 'privacy'] + fields = [ + 'user', 'book', 'name', 'content', 'content_warning', 'rating', + 'privacy'] class CommentForm(CustomForm): class Meta: model = models.Comment - fields = ['user', 'book', 'content', 'privacy'] + fields = ['user', 'book', 'content', 'content_warning', 'privacy'] class QuotationForm(CustomForm): class Meta: model = models.Quotation - fields = ['user', 'book', 'quote', 'content', 'privacy'] + fields = [ + 'user', 'book', 'quote', 'content', 'content_warning', 'privacy'] class ReplyForm(CustomForm): class Meta: model = models.Status - fields = ['user', 'content', 'reply_parent', 'privacy'] + fields = [ + 'user', 'content', 'content_warning', 'reply_parent', 'privacy'] class EditUserForm(CustomForm): diff --git a/bookwyrm/templates/snippets/create_status_form.html b/bookwyrm/templates/snippets/create_status_form.html index d6aa3fb3..70062db4 100644 --- a/bookwyrm/templates/snippets/create_status_form.html +++ b/bookwyrm/templates/snippets/create_status_form.html @@ -26,6 +26,16 @@
{% endif %} + +
+ + + +
+ {% if type == 'quote' %} {% else %} diff --git a/bookwyrm/templates/snippets/reply_form.html b/bookwyrm/templates/snippets/reply_form.html index d0a0f6b9..65aa3e46 100644 --- a/bookwyrm/templates/snippets/reply_form.html +++ b/bookwyrm/templates/snippets/reply_form.html @@ -6,6 +6,14 @@
+
+ + + +
diff --git a/bookwyrm/templates/snippets/status_content.html b/bookwyrm/templates/snippets/status_content.html index ff87b3b6..6e4a6b98 100644 --- a/bookwyrm/templates/snippets/status_content.html +++ b/bookwyrm/templates/snippets/status_content.html @@ -1,5 +1,14 @@ {% load bookwyrm_tags %}
+ {% if status.status_type == 'Review' %} +
+

+ {% if status.name %}{{ status.name }}
{% endif %} +

+

{% include 'snippets/stars.html' with rating=status.rating %}

+
+ {% endif %} + {% if status.content_warning %}

{{ status.content_warning }}

@@ -16,13 +25,6 @@ {% endif %} - {% if status.status_type == 'Review' %} -

- {% if status.name %}{{ status.name }}
{% endif %} - {% include 'snippets/stars.html' with rating=status.rating %} -

- {% endif %} - {% if status.quote %}
{{ status.quote }}