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/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..b1283c9d 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) @@ -181,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 ) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index f496a211..4681a670 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -9,9 +9,7 @@ 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_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,15 +176,16 @@ def handle_import_books(user, items): broadcast(user, activity) if item.rating or item.review: - review_title = "Review of {!r} on Goodreads".format( + 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, + ) if item.review else '' + + models.Review.objects.create( + user=user, + book=item.book, + name=review_title, + content=item.review, + rating=item.rating, ) for read in item.reads: read.book = item.book @@ -209,44 +208,25 @@ 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() + + # 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): @@ -265,21 +245,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/static/css/fonts/icomoon.eot b/bookwyrm/static/css/fonts/icomoon.eot index 5568c0ad..99a90268 100644 Binary files a/bookwyrm/static/css/fonts/icomoon.eot and b/bookwyrm/static/css/fonts/icomoon.eot differ diff --git a/bookwyrm/static/css/fonts/icomoon.svg b/bookwyrm/static/css/fonts/icomoon.svg index 4324502b..28612eee 100644 --- a/bookwyrm/static/css/fonts/icomoon.svg +++ b/bookwyrm/static/css/fonts/icomoon.svg @@ -7,27 +7,28 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/bookwyrm/static/css/fonts/icomoon.ttf b/bookwyrm/static/css/fonts/icomoon.ttf index a61117da..0f78404f 100644 Binary files a/bookwyrm/static/css/fonts/icomoon.ttf and b/bookwyrm/static/css/fonts/icomoon.ttf differ diff --git a/bookwyrm/static/css/fonts/icomoon.woff b/bookwyrm/static/css/fonts/icomoon.woff index 1dccbcf9..dcb8dc75 100644 Binary files a/bookwyrm/static/css/fonts/icomoon.woff and b/bookwyrm/static/css/fonts/icomoon.woff differ diff --git a/bookwyrm/static/css/icons.css b/bookwyrm/static/css/icons.css index eabfb06e..bf68b7d6 100644 --- a/bookwyrm/static/css/icons.css +++ b/bookwyrm/static/css/icons.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?v0wquk'); - src: url('fonts/icomoon.eot?v0wquk#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?v0wquk') format('truetype'), - url('fonts/icomoon.woff?v0wquk') format('woff'), - url('fonts/icomoon.svg?v0wquk#icomoon') format('svg'); + src: url('fonts/icomoon.eot?7ifunb'); + src: url('fonts/icomoon.eot?7ifunb#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?7ifunb') format('truetype'), + url('fonts/icomoon.woff?7ifunb') format('woff'), + url('fonts/icomoon.svg?7ifunb#icomoon') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -13,7 +13,7 @@ [class^="icon-"], [class*=" icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'icomoon' !important; - speak: none; + speak: never; font-style: normal; font-weight: normal; font-variant: normal; @@ -25,26 +25,71 @@ -moz-osx-font-smoothing: grayscale; } -.icon-arrow-right:before { +.icon-envelope:before { content: "\e900"; } -.icon-arrow-left:before { - content: "\e910"; +.icon-arrow-right:before { + content: "\e901"; } -.icon-arrow-up:before { - content: "\e911"; -} -.icon-arrow-down:before { - content: "\e912"; +.icon-bell:before { + content: "\e902"; } .icon-x:before { - content: "\e902"; + content: "\e903"; } -.icon-cancel:before { - content: "\e902"; +.icon-quote-close:before { + content: "\e904"; } -.icon-close:before { - content: "\e902"; +.icon-quote-open:before { + content: "\e905"; +} +.icon-image:before { + content: "\e906"; +} +.icon-pencil:before { + content: "\e907"; +} +.icon-list:before { + content: "\e908"; +} +.icon-unlock:before { + content: "\e909"; +} +.icon-globe:before { + content: "\e90a"; +} +.icon-lock:before { + content: "\e90b"; +} +.icon-chain-broken:before { + content: "\e90c"; +} +.icon-chain:before { + content: "\e90d"; +} +.icon-comments:before { + content: "\e90e"; +} +.icon-comment:before { + content: "\e90f"; +} +.icon-boost:before { + content: "\e910"; +} +.icon-arrow-left:before { + content: "\e911"; +} +.icon-arrow-up:before { + content: "\e912"; +} +.icon-arrow-down:before { + content: "\e913"; +} +.icon-home:before { + content: "\e914"; +} +.icon-local:before { + content: "\e915"; } .icon-search:before { content: "\e986"; @@ -61,78 +106,3 @@ .icon-heart:before { content: "\e9da"; } -.icon-local:before { - content: "\e914"; -} -.icon-home:before { - content: "\e913"; -} -.icon-quote-close:before { - content: "\e903"; -} -.icon-quote-open:before { - content: "\e904"; -} -.icon-image:before { - content: "\e905"; -} -.icon-photo:before { - content: "\e905"; -} -.icon-picture-o:before { - content: "\e905"; -} -.icon-pencil:before { - content: "\e906"; -} -.icon-list:before { - content: "\e907"; -} -.icon-unlock:before { - content: "\e908"; -} -.icon-unlisted:before { - content: "\e908"; -} -.icon-globe:before { - content: "\e909"; -} -.icon-global:before { - content: "\e909"; -} -.icon-federated:before { - content: "\e909"; -} -.icon-public:before { - content: "\e909"; -} -.icon-lock:before { - content: "\e90a"; -} -.icon-private:before { - content: "\e90a"; -} -.icon-chain-broken:before { - content: "\e90b"; -} -.icon-unlink:before { - content: "\e90b"; -} -.icon-chain:before { - content: "\e90c"; -} -.icon-link:before { - content: "\e90c"; -} -.icon-comments:before { - content: "\e90d"; -} -.icon-comment:before { - content: "\e90e"; -} -.icon-boost:before { - content: "\e90f"; -} -.icon-bell:before { - content: "\e901"; -} diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 25619839..258734b3 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -13,100 +13,6 @@ 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: - raise ValueError('Invalid rating') - return models.Review.objects.create( - user=user, - book=book, - rating=rating, - ) - - -def create_review(user, book, name, content, rating): - ''' a book review has been added ''' - name = sanitize(name) - content = sanitize(content) - - # no ratings outside of 0-5 - if rating: - rating = rating if 1 <= rating <= 5 else None - else: - rating = None - - return models.Review.objects.create( - user=user, - book=book, - name=name, - rating=rating, - content=content, - ) - - -def create_quotation_from_activity(author, activity): - ''' parse an activity json blob into a status ''' - book_id = activity['inReplyToBook'] - book = get_or_create_book(book_id) - quote = activity.get('quote') - content = activity.get('content') - published = activity.get('published') - remote_id = activity['id'] - - quotation = create_quotation(author, book, content, quote) - quotation.published_date = published - quotation.remote_id = remote_id - quotation.save() - return quotation - - -def create_quotation(user, book, content, quote): - ''' a quotation has been added ''' - # throws a value error if the book is not found - content = sanitize(content) - quote = sanitize(quote) - - return models.Quotation.objects.create( - user=user, - book=book, - content=content, - quote=quote, - ) - - -def create_comment_from_activity(author, activity): - ''' parse an activity json blob into a status ''' - book_id = activity['inReplyToBook'] - book = get_or_create_book(book_id) - content = activity.get('content') - published = activity.get('published') - remote_id = activity['id'] - - comment = create_comment(author, book, content) - comment.published_date = published - comment.remote_id = remote_id - comment.save() - return comment - - -def create_comment(user, book, content): - ''' a book comment has been added ''' - # throws a value error if the book is not found - content = sanitize(content) - - return models.Comment.objects.create( - user=user, - book=book, - content=content, - ) - - -def get_status(remote_id): - ''' find a status in the database ''' - return models.Status.objects.select_subclasses().filter( - remote_id=remote_id - ).first() - def create_generated_note(user, content, mention_books=None): ''' a note created by the app about user activity ''' @@ -127,28 +33,6 @@ def create_generated_note(user, content, mention_books=None): return status -def create_status(user, content, reply_parent=None, mention_books=None): - ''' a status update ''' - # TODO: handle @'ing users - - # sanitize input html - parser = InputHtmlParser() - parser.feed(content) - content = parser.get_output() - - status = models.Status.objects.create( - user=user, - content=content, - reply_parent=reply_parent, - ) - - if mention_books: - for book in mention_books: - status.mention_books.add(book) - - return status - - def create_tag(user, possible_book, name): ''' add a tag to a book ''' book = get_or_create_book(possible_book) @@ -174,10 +58,3 @@ def create_notification(user, notification_type, related_user=None, \ related_import=related_import, notification_type=notification_type, ) - - -def sanitize(content): - ''' remove invalid html from free text ''' - parser = InputHtmlParser() - parser.feed(content) - return parser.get_output() diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index f5f72e65..55380652 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -47,11 +47,11 @@ @@ -51,11 +62,24 @@ @@ -64,6 +88,9 @@ diff --git a/bookwyrm/templates/snippets/interaction.html b/bookwyrm/templates/snippets/interaction.html index 8680df8d..a48d8a7c 100644 --- a/bookwyrm/templates/snippets/interaction.html +++ b/bookwyrm/templates/snippets/interaction.html @@ -4,7 +4,9 @@
{% csrf_token %} - + + + diff --git a/bookwyrm/tests/status/__init__.py b/bookwyrm/tests/status/__init__.py deleted file mode 100644 index b6e690fd..00000000 --- a/bookwyrm/tests/status/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import * diff --git a/bookwyrm/tests/status/test_comment.py b/bookwyrm/tests/status/test_comment.py deleted file mode 100644 index be127d88..00000000 --- a/bookwyrm/tests/status/test_comment.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.test import TestCase - -from bookwyrm import models -from bookwyrm import status as status_builder - - -class Comment(TestCase): - ''' we have hecka ways to create statuses ''' - def setUp(self): - self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword') - self.book = models.Edition.objects.create(title='Example Edition') - - - def test_create_comment(self): - comment = status_builder.create_comment( - self.user, self.book, 'commentary') - self.assertEqual(comment.content, 'commentary') diff --git a/bookwyrm/tests/status/test_quotation.py b/bookwyrm/tests/status/test_quotation.py deleted file mode 100644 index 4892e21d..00000000 --- a/bookwyrm/tests/status/test_quotation.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.test import TestCase - -from bookwyrm import models -from bookwyrm import status as status_builder - - -class Quotation(TestCase): - ''' we have hecka ways to create statuses ''' - def setUp(self): - self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - remote_id='https://example.com/user/mouse' - ) - self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - ) - - - def test_create_quotation(self): - quotation = status_builder.create_quotation( - self.user, self.book, 'commentary', 'a quote') - self.assertEqual(quotation.quote, 'a quote') - self.assertEqual(quotation.content, 'commentary') diff --git a/bookwyrm/tests/status/test_review.py b/bookwyrm/tests/status/test_review.py deleted file mode 100644 index 263fef97..00000000 --- a/bookwyrm/tests/status/test_review.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.test import TestCase - -from bookwyrm import models -from bookwyrm import status as status_builder - - -class Review(TestCase): - ''' we have hecka ways to create statuses ''' - def setUp(self): - self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword') - self.book = models.Edition.objects.create(title='Example Edition') - - - def test_create_review(self): - review = status_builder.create_review( - self.user, self.book, 'review name', 'content', 5) - self.assertEqual(review.name, 'review name') - self.assertEqual(review.content, 'content') - self.assertEqual(review.rating, 5) - - review = status_builder.create_review( - self.user, self.book, '
review
name', 'content', 5) - self.assertEqual(review.name, 'review name') - self.assertEqual(review.content, 'content') - self.assertEqual(review.rating, 5) - - def test_review_rating(self): - review = status_builder.create_review( - self.user, self.book, 'review name', 'content', -1) - self.assertEqual(review.name, 'review name') - self.assertEqual(review.content, 'content') - self.assertEqual(review.rating, None) - - review = status_builder.create_review( - self.user, self.book, 'review name', 'content', 6) - self.assertEqual(review.name, 'review name') - self.assertEqual(review.content, 'content') - self.assertEqual(review.rating, None) diff --git a/bookwyrm/tests/status/test_status.py b/bookwyrm/tests/status/test_status.py deleted file mode 100644 index cb49cb12..00000000 --- a/bookwyrm/tests/status/test_status.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.test import TestCase - -from bookwyrm import models -from bookwyrm import status as status_builder - - -class Status(TestCase): - ''' we have hecka ways to create statuses ''' - def setUp(self): - self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=False, - inbox='https://example.com/user/mouse/inbox', - outbox='https://example.com/user/mouse/outbox', - remote_id='https://example.com/user/mouse' - ) - - - def test_create_status(self): - content = 'statuses are usually replies' - status = status_builder.create_status( - self.user, content) - self.assertEqual(status.content, content) - - reply = status_builder.create_status( - self.user, content, reply_parent=status) - self.assertEqual(reply.content, content) - self.assertEqual(reply.reply_parent, status) diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 3d01e9ff..e8c3e6f2 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -289,68 +289,45 @@ 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) + + +@login_required +def reply(request): + ''' respond to a book review ''' + form = forms.ReplyForm(request.POST) + return handle_status(request, form) + + +def handle_status(request, form): + ''' all the "create a status" 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) + return redirect(request.headers.get('Referer', '/')) - # TODO: validation, htmlification - content = form.data.get('content') - - outgoing.handle_comment(request.user, book_id, content) - return redirect('/book/%s' % book_id) + outgoing.handle_status(request.user, form) + return redirect(request.headers.get('Referer', '/')) @login_required @@ -376,19 +353,6 @@ def untag(request): return redirect('/book/%s' % book_id) -@login_required -def reply(request): - ''' respond to a book review ''' - form = forms.ReplyForm(request.POST) - # this is a bit of a formality, the form is just one text field - if not form.is_valid(): - return redirect('/') - parent_id = request.POST['parent'] - parent = models.Status.objects.get(id=parent_id) - outgoing.handle_reply(request.user, parent, form.data['content']) - return redirect('/') - - @login_required def favorite(request, status_id): ''' like a status ''' diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 55957ff2..2422cd27 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -130,17 +130,22 @@ def get_activity_feed(user, filter_level, model=models.Status): if filter_level in ['friends', 'home']: # people you follow and direct mentions activities = activities.filter( - Q(user__in=following, privacy='public') | \ - Q(mention_users=user) + Q(user__in=following, privacy__in=['public', 'unlisted', 'followers']) | \ + Q(mention_users=user) | Q(user=user) ) elif filter_level == 'self': activities = activities.filter(user=user, privacy='public') elif filter_level == 'local': - # everyone on this instance - activities = activities.filter(user__local=True, privacy='public') + # everyone on this instance except unlisted + activities = activities.filter( + Q(user__in=following, privacy='followers') | Q(privacy='public'), + user__local=True + ) else: # all activities from everyone you federate with - activities = activities.filter(privacy='public') + activities = activities.filter( + Q(user__in=following, privacy='followers') | Q(privacy='public') + ) return activities @@ -386,9 +391,14 @@ def status_page(request, username, status_id): except ValueError: return HttpResponseNotFound() + # the url should have the poster's username in it if user != status.user: return HttpResponseNotFound() + # make sure the user is authorized to see the status + if not status_visible_to_user(request.user, status): + return HttpResponseNotFound() + if is_api_request(request): return JsonResponse(status.to_activity(), encoder=ActivityEncoder) @@ -397,6 +407,19 @@ def status_page(request, username, status_id): } return TemplateResponse(request, 'status.html', data) +def status_visible_to_user(viewer, status): + ''' is a user authorized to view a status? ''' + if viewer == status.user or status.privacy in ['public', 'unlisted']: + return True + if status.privacy == 'followers' and \ + status.user.followers.filter(id=viewer.id).first(): + return True + if status.privacy == 'direct' and \ + status.mention_users.filter(id=viewer.id).first(): + return True + return False + + @csrf_exempt def replies_page(request, username, status_id):