Merge pull request #1508 from bookwyrm-social/privacy_filter

Privacy filter
This commit is contained in:
Mouse Reeve 2021-10-06 11:34:29 -07:00 committed by GitHub
commit 15fb20012b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 133 additions and 128 deletions

View File

@ -8,7 +8,6 @@ from django.utils import timezone
from bookwyrm import models from bookwyrm import models
from bookwyrm.redis_store import RedisStore, r from bookwyrm.redis_store import RedisStore, r
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.views.helpers import privacy_filter
class ActivityStream(RedisStore): class ActivityStream(RedisStore):
@ -43,7 +42,7 @@ class ActivityStream(RedisStore):
def add_user_statuses(self, viewer, user): def add_user_statuses(self, viewer, user):
"""add a user's statuses to another user's feed""" """add a user's statuses to another user's feed"""
# only add the statuses that the viewer should be able to see (ie, not dms) # only add the statuses that the viewer should be able to see (ie, not dms)
statuses = privacy_filter(viewer, user.status_set.all()) statuses = models.Status.privacy_filter(viewer).filter(user=user)
self.bulk_add_objects_to_store(statuses, self.stream_id(viewer)) self.bulk_add_objects_to_store(statuses, self.stream_id(viewer))
def remove_user_statuses(self, viewer, user): def remove_user_statuses(self, viewer, user):
@ -113,9 +112,8 @@ class ActivityStream(RedisStore):
def get_statuses_for_user(self, user): # pylint: disable=no-self-use def get_statuses_for_user(self, user): # pylint: disable=no-self-use
"""given a user, what statuses should they see on this stream""" """given a user, what statuses should they see on this stream"""
return privacy_filter( return models.Status.privacy_filter(
user, user,
models.Status.objects.select_subclasses(),
privacy_levels=["public", "unlisted", "followers"], privacy_levels=["public", "unlisted", "followers"],
) )
@ -139,11 +137,15 @@ class HomeStream(ActivityStream):
).distinct() ).distinct()
def get_statuses_for_user(self, user): def get_statuses_for_user(self, user):
return privacy_filter( return models.Status.privacy_filter(
user, user,
models.Status.objects.select_subclasses(),
privacy_levels=["public", "unlisted", "followers"], privacy_levels=["public", "unlisted", "followers"],
following_only=True, ).exclude(
~Q( # remove everything except
Q(user__followers=user) # user following
| Q(user=user) # is self
| Q(mention_users=user) # mentions user
),
) )
@ -160,11 +162,10 @@ class LocalStream(ActivityStream):
def get_statuses_for_user(self, user): def get_statuses_for_user(self, user):
# all public statuses by a local user # all public statuses by a local user
return privacy_filter( return models.Status.privacy_filter(
user, user,
models.Status.objects.select_subclasses().filter(user__local=True),
privacy_levels=["public"], privacy_levels=["public"],
) ).filter(user__local=True)
class BooksStream(ActivityStream): class BooksStream(ActivityStream):
@ -197,50 +198,53 @@ class BooksStream(ActivityStream):
books = user.shelfbook_set.values_list( books = user.shelfbook_set.values_list(
"book__parent_work__id", flat=True "book__parent_work__id", flat=True
).distinct() ).distinct()
return privacy_filter( return (
user, models.Status.privacy_filter(
models.Status.objects.select_subclasses() user,
privacy_levels=["public"],
)
.filter( .filter(
Q(comment__book__parent_work__id__in=books) Q(comment__book__parent_work__id__in=books)
| Q(quotation__book__parent_work__id__in=books) | Q(quotation__book__parent_work__id__in=books)
| Q(review__book__parent_work__id__in=books) | Q(review__book__parent_work__id__in=books)
| Q(mention_books__parent_work__id__in=books) | Q(mention_books__parent_work__id__in=books)
) )
.distinct(), .distinct()
privacy_levels=["public"],
) )
def add_book_statuses(self, user, book): def add_book_statuses(self, user, book):
"""add statuses about a book to a user's feed""" """add statuses about a book to a user's feed"""
work = book.parent_work work = book.parent_work
statuses = privacy_filter( statuses = (
user, models.Status.privacy_filter(
models.Status.objects.select_subclasses() user,
privacy_levels=["public"],
)
.filter( .filter(
Q(comment__book__parent_work=work) Q(comment__book__parent_work=work)
| Q(quotation__book__parent_work=work) | Q(quotation__book__parent_work=work)
| Q(review__book__parent_work=work) | Q(review__book__parent_work=work)
| Q(mention_books__parent_work=work) | Q(mention_books__parent_work=work)
) )
.distinct(), .distinct()
privacy_levels=["public"],
) )
self.bulk_add_objects_to_store(statuses, self.stream_id(user)) self.bulk_add_objects_to_store(statuses, self.stream_id(user))
def remove_book_statuses(self, user, book): def remove_book_statuses(self, user, book):
"""add statuses about a book to a user's feed""" """add statuses about a book to a user's feed"""
work = book.parent_work work = book.parent_work
statuses = privacy_filter( statuses = (
user, models.Status.privacy_filter(
models.Status.objects.select_subclasses() user,
privacy_levels=["public"],
)
.filter( .filter(
Q(comment__book__parent_work=work) Q(comment__book__parent_work=work)
| Q(quotation__book__parent_work=work) | Q(quotation__book__parent_work=work)
| Q(review__book__parent_work=work) | Q(review__book__parent_work=work)
| Q(mention_books__parent_work=work) | Q(mention_books__parent_work=work)
) )
.distinct(), .distinct()
privacy_levels=["public"],
) )
self.bulk_remove_objects_from_store(statuses, self.stream_id(user)) self.bulk_remove_objects_from_store(statuses, self.stream_id(user))

View File

@ -4,6 +4,7 @@ from Crypto import Random
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -105,6 +106,52 @@ class BookWyrmModel(models.Model):
raise PermissionDenied() raise PermissionDenied()
@classmethod
def privacy_filter(cls, viewer, privacy_levels=None):
"""filter objects that have "user" and "privacy" fields"""
queryset = cls.objects
if hasattr(queryset, "select_subclasses"):
queryset = queryset.select_subclasses()
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]
# you can't see followers only or direct messages if you're not logged in
if viewer.is_anonymous:
privacy_levels = [
p for p in privacy_levels if not p in ["followers", "direct"]
]
else:
# exclude blocks from both directions
queryset = queryset.exclude(
Q(user__blocked_by=viewer) | Q(user__blocks=viewer)
)
# filter to only provided privacy levels
queryset = queryset.filter(privacy__in=privacy_levels)
if "followers" in privacy_levels:
queryset = cls.followers_filter(queryset, viewer)
# exclude direct messages not intended for the user
if "direct" in privacy_levels:
queryset = cls.direct_filter(queryset, viewer)
return queryset
@classmethod
def followers_filter(cls, queryset, viewer):
"""Override-able filter for "followers" privacy level"""
return queryset.exclude(
~Q( # user isn't following and it isn't their own status
Q(user__followers=viewer) | Q(user=viewer)
),
privacy="followers", # and the status is followers only
)
@classmethod
def direct_filter(cls, queryset, viewer):
"""Override-able filter for "direct" privacy level"""
return queryset.exclude(~Q(user=viewer), privacy="direct")
@receiver(models.signals.post_save) @receiver(models.signals.post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -6,6 +6,7 @@ from django.apps import apps
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.template.loader import get_template from django.template.loader import get_template
from django.utils import timezone from django.utils import timezone
@ -207,6 +208,18 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
if isinstance(self, (GeneratedNote, ReviewRating)): if isinstance(self, (GeneratedNote, ReviewRating)):
raise PermissionDenied() raise PermissionDenied()
@classmethod
def privacy_filter(cls, viewer, privacy_levels=None):
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
return queryset.filter(deleted=False)
@classmethod
def direct_filter(cls, queryset, viewer):
"""Overridden filter for "direct" privacy level"""
return queryset.exclude(
~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy="direct"
)
class GeneratedNote(Status): class GeneratedNote(Status):
"""these are app-generated messages about user activity""" """these are app-generated messages about user activity"""

View File

@ -2,7 +2,7 @@
from django import template from django import template
from django.db.models import Avg from django.db.models import Avg
from bookwyrm import models, views from bookwyrm import models
register = template.Library() register = template.Library()
@ -11,8 +11,8 @@ register = template.Library()
@register.filter(name="rating") @register.filter(name="rating")
def get_rating(book, user): def get_rating(book, user):
"""get the overall rating of a book""" """get the overall rating of a book"""
queryset = views.helpers.privacy_filter( queryset = models.Review.privacy_filter(user).filter(
user, models.Review.objects.filter(book__parent_work__editions=book) book__parent_work__editions=book
) )
return queryset.aggregate(Avg("rating"))["rating__avg"] return queryset.aggregate(Avg("rating"))["rating__avg"]

View File

@ -16,7 +16,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import get_image from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request, privacy_filter from bookwyrm.views.helpers import is_api_request
# pylint: disable=no-self-use # pylint: disable=no-self-use
@ -48,8 +48,8 @@ class Book(View):
raise Http404() raise Http404()
# all reviews for all editions of the book # all reviews for all editions of the book
reviews = privacy_filter( reviews = models.Review.privacy_filter(request.user).filter(
request.user, models.Review.objects.filter(book__parent_work__editions=book) book__parent_work__editions=book
) )
# the reviews to show # the reviews to show
@ -66,12 +66,9 @@ class Book(View):
queryset = queryset.select_related("user").order_by("-published_date") queryset = queryset.select_related("user").order_by("-published_date")
paginated = Paginator(queryset, PAGE_LENGTH) paginated = Paginator(queryset, PAGE_LENGTH)
lists = privacy_filter( lists = models.List.privacy_filter(request.user,).filter(
request.user, listitem__approved=True,
models.List.objects.filter( listitem__book__in=book.parent_work.editions.all(),
listitem__approved=True,
listitem__book__in=book.parent_work.editions.all(),
),
) )
data = { data = {
"book": book, "book": book,

View File

@ -13,7 +13,7 @@ from bookwyrm import activitystreams, forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH, STREAMS from bookwyrm.settings import PAGE_LENGTH, STREAMS
from bookwyrm.suggested_users import suggested_users from bookwyrm.suggested_users import suggested_users
from .helpers import get_user_from_username, privacy_filter from .helpers import get_user_from_username
from .helpers import is_api_request, is_bookwyrm_request from .helpers import is_api_request, is_bookwyrm_request
@ -56,11 +56,15 @@ class DirectMessage(View):
def get(self, request, username=None): def get(self, request, username=None):
"""like a feed but for dms only""" """like a feed but for dms only"""
# remove fancy subclasses of status, keep just good ol' notes # remove fancy subclasses of status, keep just good ol' notes
queryset = models.Status.objects.filter( activities = (
review__isnull=True, models.Status.privacy_filter(request.user, privacy_levels=["direct"])
comment__isnull=True, .filter(
quotation__isnull=True, review__isnull=True,
generatednote__isnull=True, comment__isnull=True,
quotation__isnull=True,
generatednote__isnull=True,
)
.order_by("-published_date")
) )
user = None user = None
@ -70,11 +74,7 @@ class DirectMessage(View):
except Http404: except Http404:
pass pass
if user: if user:
queryset = queryset.filter(Q(user=user) | Q(mention_users=user)) activities = activities.filter(Q(user=user) | Q(mention_users=user))
activities = privacy_filter(
request.user, queryset, privacy_levels=["direct"]
).order_by("-published_date")
paginated = Paginator(activities, PAGE_LENGTH) paginated = Paginator(activities, PAGE_LENGTH)
data = { data = {
@ -109,9 +109,11 @@ class Status(View):
status.to_activity(pure=not is_bookwyrm_request(request)) status.to_activity(pure=not is_bookwyrm_request(request))
) )
visible_thread = privacy_filter( visible_thread = (
request.user, models.Status.objects.filter(thread_id=status.thread_id) models.Status.privacy_filter(request.user)
).values_list("id", flat=True) .filter(thread_id=status.thread_id)
.values_list("id", flat=True)
)
visible_thread = list(visible_thread) visible_thread = list(visible_thread)
ancestors = models.Status.objects.select_subclasses().raw( ancestors = models.Status.objects.select_subclasses().raw(

View File

@ -6,8 +6,6 @@ import dateutil.tz
from dateutil.parser import ParserError from dateutil.parser import ParserError
from requests import HTTPError from requests import HTTPError
from django.core.exceptions import FieldError
from django.db.models import Q
from django.http import Http404 from django.http import Http404
from bookwyrm import activitypub, models from bookwyrm import activitypub, models
@ -50,56 +48,6 @@ def is_bookwyrm_request(request):
return True return True
def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
"""filter objects that have "user" and "privacy" fields"""
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]
# if there'd a deleted field, exclude deleted items
try:
queryset = queryset.filter(deleted=False)
except FieldError:
pass
# exclude blocks from both directions
if not viewer.is_anonymous:
queryset = queryset.exclude(Q(user__blocked_by=viewer) | Q(user__blocks=viewer))
# you can't see followers only or direct messages if you're not logged in
if viewer.is_anonymous:
privacy_levels = [p for p in privacy_levels if not p in ["followers", "direct"]]
# filter to only privided privacy levels
queryset = queryset.filter(privacy__in=privacy_levels)
# only include statuses the user follows
if following_only:
queryset = queryset.exclude(
~Q( # remove everythign except
Q(user__followers=viewer)
| Q(user=viewer) # user following
| Q(mention_users=viewer) # is self # mentions user
),
)
# exclude followers-only statuses the user doesn't follow
elif "followers" in privacy_levels:
queryset = queryset.exclude(
~Q( # user isn't following and it isn't their own status
Q(user__followers=viewer) | Q(user=viewer)
),
privacy="followers", # and the status is followers only
)
# exclude direct messages not intended for the user
if "direct" in privacy_levels:
try:
queryset = queryset.exclude(
~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy="direct"
)
except FieldError:
queryset = queryset.exclude(~Q(user=viewer), privacy="direct")
return queryset
def handle_remote_webfinger(query): def handle_remote_webfinger(query):
"""webfingerin' other servers""" """webfingerin' other servers"""
user = None user = None

View File

@ -18,7 +18,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import book_search, forms, models from bookwyrm import book_search, forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, privacy_filter from .helpers import is_api_request
from .helpers import get_user_from_username from .helpers import get_user_from_username
@ -30,9 +30,10 @@ class Lists(View):
"""display a book list""" """display a book list"""
# hide lists with no approved books # hide lists with no approved books
lists = ( lists = (
models.List.objects.annotate( models.List.privacy_filter(
item_count=Count("listitem", filter=Q(listitem__approved=True)) request.user, privacy_levels=["public", "followers"]
) )
.annotate(item_count=Count("listitem", filter=Q(listitem__approved=True)))
.filter(item_count__gt=0) .filter(item_count__gt=0)
.select_related("user") .select_related("user")
.prefetch_related("listitem_set") .prefetch_related("listitem_set")
@ -40,10 +41,6 @@ class Lists(View):
.distinct() .distinct()
) )
lists = privacy_filter(
request.user, lists, privacy_levels=["public", "followers"]
)
paginated = Paginator(lists, 12) paginated = Paginator(lists, 12)
data = { data = {
"lists": paginated.get_page(request.GET.get("page")), "lists": paginated.get_page(request.GET.get("page")),
@ -89,8 +86,7 @@ class UserLists(View):
def get(self, request, username): def get(self, request, username):
"""display a book list""" """display a book list"""
user = get_user_from_username(request.user, username) user = get_user_from_username(request.user, username)
lists = models.List.objects.filter(user=user) lists = models.List.privacy_filter(request.user).filter(user=user)
lists = privacy_filter(request.user, lists)
paginated = Paginator(lists, 12) paginated = Paginator(lists, 12)
data = { data = {

View File

@ -4,7 +4,8 @@ from django.contrib.syndication.views import Feed
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .helpers import get_user_from_username, privacy_filter from bookwyrm import models
from .helpers import get_user_from_username
# pylint: disable=no-self-use, unused-argument # pylint: disable=no-self-use, unused-argument
class RssFeed(Feed): class RssFeed(Feed):
@ -35,11 +36,10 @@ class RssFeed(Feed):
def items(self, obj): def items(self, obj):
"""the user's activity feed""" """the user's activity feed"""
return privacy_filter( return models.Status.privacy_filter(
obj, obj,
obj.status_set.select_subclasses(),
privacy_levels=["public", "unlisted"], privacy_levels=["public", "unlisted"],
) ).filter(user=obj)
def item_link(self, item): def item_link(self, item):
"""link to the status""" """link to the status"""

View File

@ -13,7 +13,7 @@ from bookwyrm.connectors import connector_manager
from bookwyrm.book_search import search, format_search_result from bookwyrm.book_search import search, format_search_result
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.utils import regex from bookwyrm.utils import regex
from .helpers import is_api_request, privacy_filter from .helpers import is_api_request
from .helpers import handle_remote_webfinger from .helpers import handle_remote_webfinger
@ -108,9 +108,8 @@ def user_search(query, viewer, *_):
def list_search(query, viewer, *_): def list_search(query, viewer, *_):
"""any relevent lists?""" """any relevent lists?"""
return ( return (
privacy_filter( models.List.privacy_filter(
viewer, viewer,
models.List.objects,
privacy_levels=["public", "followers"], privacy_levels=["public", "followers"],
) )
.annotate( .annotate(

View File

@ -17,7 +17,6 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_user_from_username from .helpers import is_api_request, get_user_from_username
from .helpers import privacy_filter
# pylint: disable=no-self-use # pylint: disable=no-self-use
@ -33,7 +32,7 @@ class Shelf(View):
if is_self: if is_self:
shelves = user.shelf_set.all() shelves = user.shelf_set.all()
else: else:
shelves = privacy_filter(request.user, user.shelf_set).all() shelves = models.Shelf.privacy_filter(request.user).filter(user=user).all()
# get the shelf and make sure the logged in user should be able to see it # get the shelf and make sure the logged in user should be able to see it
if shelf_identifier: if shelf_identifier:
@ -58,16 +57,17 @@ class Shelf(View):
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(shelf.to_activity(**request.GET)) return ActivitypubResponse(shelf.to_activity(**request.GET))
reviews = models.Review.objects.filter( reviews = models.Review.objects
if not is_self:
reviews = models.Review.privacy_filter(request.user)
reviews = reviews.filter(
user=user, user=user,
rating__isnull=False, rating__isnull=False,
book__id=OuterRef("id"), book__id=OuterRef("id"),
deleted=False, deleted=False,
).order_by("-published_date") ).order_by("-published_date")
if not is_self:
reviews = privacy_filter(request.user, reviews)
books = books.annotate( books = books.annotate(
rating=Subquery(reviews.values("rating")[:1]), rating=Subquery(reviews.values("rating")[:1]),
shelved_date=F("shelfbook__shelved_date"), shelved_date=F("shelfbook__shelved_date"),

View File

@ -12,7 +12,6 @@ from bookwyrm import models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_user_from_username, is_api_request from .helpers import get_user_from_username, is_api_request
from .helpers import privacy_filter
# pylint: disable=no-self-use # pylint: disable=no-self-use
@ -56,10 +55,10 @@ class User(View):
# user's posts # user's posts
activities = ( activities = (
privacy_filter( models.Status.privacy_filter(
request.user, request.user,
user.status_set.select_subclasses(),
) )
.filter(user=user)
.select_related( .select_related(
"user", "user",
"reply_parent", "reply_parent",