diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 60e5da0ad..cb2fc851e 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -34,7 +34,7 @@ class BookWyrmModel(models.Model): @receiver(models.signals.post_save) # pylint: disable=unused-argument -def execute_after_save(sender, instance, created, *args, **kwargs): +def set_remote_id(sender, instance, created, *args, **kwargs): """ set the remote_id after save (when the id is available) """ if not created or not hasattr(instance, "get_remote_id"): return diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 904ce461d..6d93ff0a2 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -5,11 +5,14 @@ import re from django.apps import apps from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.db.models import Q +from django.dispatch import receiver from django.template.loader import get_template from django.utils import timezone from model_utils.managers import InheritanceManager +import redis -from bookwyrm import activitypub +from bookwyrm import activitypub, settings from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import OrderedCollectionPageMixin from .base_model import BookWyrmModel @@ -17,6 +20,10 @@ from .fields import image_serializer from .readthrough import ProgressMode from . import fields +r = redis.Redis( + host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0 +) + class Status(OrderedCollectionPageMixin, BookWyrmModel): """ any post, like a reply to a review, etc """ @@ -383,3 +390,71 @@ class Boost(ActivityMixin, Status): # This constraint can't work as it would cross tables. # class Meta: # unique_together = ('user', 'boosted_status') + + +@receiver(models.signals.post_save) +# pylint: disable=unused-argument +def update_feeds(sender, instance, created, *args, **kwargs): + """ add statuses to activity feeds """ + # we're only interested in new statuses that aren't dms + if not created or not issubclass(sender, Status) or instance.privacy == 'direct': + return + + user = instance.user + + community = user.__class__.objects.filter( + local=True # we only manage timelines for local users + ).exclude( + Q(id__in=user.blocks.all()) | Q(blocks=user) # not blocked + ) + + # ------ home timeline: users you follow and yourself + friends = community.filter( + Q(id=user.id) | Q(following=user) + ) + add_status(friends, instance, 'home') + + # local and federated timelines only get public statuses + if instance.privacy != 'public': + return + + # ------ federated timeline: to anyone, anywhere + add_status(community, instance, 'federated') + + # if the author is a remote user, it doesn't go on the local timeline + if not user.local: + return + + # ------ local timeline: to anyone, anywhere + add_status(community, instance, 'local') + + +def add_status(users, status, feed_name): + """ add a status to users' feeds """ + # we want to do this as a bulk operation + pipeline = r.pipeline() + value = {status.id: status.published_date.timestamp()} + for user in users: + feed_id = '{}-{}'.format(user.id, feed_name) + unread_feed_id = '{}-unread'.format(feed_id) + + # add the status to the feed + pipeline.zadd(feed_id, value) + + # add to the unread status count + pipeline.incr(unread_feed_id) + pipeline.execute() + + +def get_activity_stream(user, feed_name, start, end): + """ load the ids for statuses to be displayed """ + feed_id = '{}-{}'.format(user.id, feed_name) + unread_feed_id = '{}-unread'.format(feed_id) + + # clear unreads for this feed + r.set(unread_feed_id, 0) + + statuses = r.zrange(feed_id, start, end) + return Status.objects.select_subclasses().filter( + id__in=statuses + ).order_by('-published_date') diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index cd8448503..bc210dacc 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -92,10 +92,12 @@ TEMPLATES = [ WSGI_APPLICATION = "bookwyrm.wsgi.application" -# redis +# redis/activity streams settings REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost") REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379) +MAX_STREAM_LENGTH = env("MAX_STREAM_LENGTH", 200) + # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index d08c9a42c..a19c2738a 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -28,19 +28,22 @@ class Feed(View): except ValueError: page = 1 - if tab == "home": - activities = get_activity_feed(request.user, following_only=True) + try: + tab_title = { + 'home': _("Home"), + "local": _("Local"), + "federated": _("Federated") + }[tab] + except KeyError: + tab = 'home' tab_title = _("Home") - elif tab == "local": - activities = get_activity_feed( - request.user, privacy=["public", "followers"], local_only=True - ) - tab_title = _("Local") - else: - activities = get_activity_feed( - request.user, privacy=["public", "followers"] - ) - tab_title = _("Federated") + + activities = models.status.get_activity_stream( + request.user, tab, + (1 - page) * PAGE_LENGTH, + page * PAGE_LENGTH + ) + paginated = Paginator(activities, PAGE_LENGTH) data = {