bookwyrm/bookwyrm/activitystreams.py

180 lines
5.7 KiB
Python
Raw Normal View History

2021-03-23 01:39:16 +00:00
""" access the activity streams stored in redis """
from abc import ABC
from django.dispatch import receiver
from django.db.models import signals
from django.db.models import Q
import redis
from bookwyrm import models, settings
from bookwyrm.views.helpers import privacy_filter
r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
)
class ActivityStream(ABC):
""" a category of activity stream (like home, local, federated) """
def stream_id(self, user):
""" the redis key for this user's instance of this stream """
2021-03-23 01:54:17 +00:00
return "{}-{}".format(user.id, self.key)
2021-03-23 01:39:16 +00:00
def unread_id(self, user):
""" the redis key for this user's unread count for this stream """
2021-03-23 01:54:17 +00:00
return "{}-unread".format(self.stream_id(user))
2021-03-23 01:39:16 +00:00
def add_status(self, status):
""" add a status to users' feeds """
value = self.get_value(status)
# we want to do this as a bulk operation, hence "pipeline"
pipeline = r.pipeline()
for user in self.stream_users(status):
# add the status to the feed
pipeline.zadd(self.stream_id(user), value)
# add to the unread status count
pipeline.incr(self.unread_id(user))
# and go!
pipeline.execute()
def get_value(self, status): # pylint: disable=no-self-use
""" the status id and the rank (ie, published date) """
return {status.id: status.published_date.timestamp()}
def get_activity_stream(self, user):
""" load the ids for statuses to be displayed """
# clear unreads for this feed
r.set(self.unread_id(user), 0)
statuses = r.zrevrange(self.stream_id(user), 0, -1)
2021-03-23 01:54:17 +00:00
return (
models.Status.objects.select_subclasses()
.filter(id__in=statuses)
.order_by("-published_date")
)
2021-03-23 01:39:16 +00:00
def populate_stream(self, user):
2021-03-23 01:54:17 +00:00
""" go from zero to a timeline """
2021-03-23 01:39:16 +00:00
pipeline = r.pipeline()
statuses = self.stream_statuses(user)
stream_id = self.stream_id(user)
2021-03-23 01:54:17 +00:00
for status in statuses.all()[: settings.MAX_STREAM_LENGTH]:
2021-03-23 01:39:16 +00:00
pipeline.zadd(stream_id, self.get_value(status))
pipeline.execute()
def stream_users(self, status): # pylint: disable=no-self-use
""" given a status, what users should see it """
2021-03-23 02:17:46 +00:00
# direct messages don't appeard in feeds, direct comments/reviews/etc do
if status.privacy == "direct" and status.status_type == 'Note':
2021-03-23 01:39:16 +00:00
return None
# everybody who could plausibly see this status
audience = models.User.objects.filter(
is_active=True,
2021-03-23 01:54:17 +00:00
local=True, # we only create feeds for users of this instance
2021-03-23 01:39:16 +00:00
).exclude(
Q(id__in=status.user.blocks.all()) | Q(blocks=status.user) # not blocked
)
# only visible to the poster's followers and tagged users
2021-03-23 01:54:17 +00:00
if status.privacy == "followers":
2021-03-23 01:39:16 +00:00
audience = audience.filter(
Q(id=status.user.id) # if the user is the post's author
2021-03-23 01:54:17 +00:00
| Q(following=status.user) # if the user is following the author
2021-03-23 01:39:16 +00:00
)
return audience
def stream_statuses(self, user): # pylint: disable=no-self-use
""" given a user, what statuses should they see on this stream """
return privacy_filter(
user,
models.Status.objects.select_subclasses(),
2021-03-23 01:54:17 +00:00
privacy_levels=["public", "unlisted", "followers"],
2021-03-23 01:39:16 +00:00
)
class HomeStream(ActivityStream):
""" users you follow """
2021-03-23 01:54:17 +00:00
key = "home"
2021-03-23 01:39:16 +00:00
def stream_users(self, status):
audience = super().stream_users(status)
return audience.filter(
Q(id=status.user.id) # if the user is the post's author
2021-03-23 01:54:17 +00:00
| Q(following=status.user) # if the user is following the author
| Q(id__in=status.mention_users.all()) # or the user is mentioned
2021-03-23 01:39:16 +00:00
)
def stream_statuses(self, user):
return privacy_filter(
user,
models.Status.objects.select_subclasses(),
2021-03-23 01:54:17 +00:00
privacy_levels=["public", "unlisted", "followers"],
following_only=True,
2021-03-23 01:39:16 +00:00
)
class LocalStream(ActivityStream):
""" users you follow """
2021-03-23 01:54:17 +00:00
key = "local"
2021-03-23 01:39:16 +00:00
def stream_users(self, status):
# this stream wants no part in non-public statuses
2021-03-23 01:54:17 +00:00
if status.privacy != "public":
2021-03-23 01:39:16 +00:00
return None
return super().stream_users(status)
def stream_statuses(self, user):
# all public statuses by a local user
return privacy_filter(
user,
models.Status.objects.select_subclasses().filter(user__local=True),
privacy_levels=["public"],
)
class FederatedStream(ActivityStream):
""" users you follow """
2021-03-23 01:54:17 +00:00
key = "federated"
2021-03-23 01:39:16 +00:00
def stream_users(self, status):
# this stream wants no part in non-public statuses
2021-03-23 01:54:17 +00:00
if status.privacy != "public":
2021-03-23 01:39:16 +00:00
return None
return super().stream_users(status)
def stream_statuses(self, user):
return privacy_filter(
user,
models.Status.objects.select_subclasses(),
privacy_levels=["public"],
)
streams = {
2021-03-23 01:54:17 +00:00
"home": HomeStream(),
"local": LocalStream(),
"federated": FederatedStream(),
2021-03-23 01:39:16 +00:00
}
2021-03-23 01:54:17 +00:00
2021-03-23 01:39:16 +00:00
@receiver(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
2021-03-23 01:54:17 +00:00
if (
not created
or not issubclass(sender, models.Status)
or instance.privacy == "direct"
):
2021-03-23 01:39:16 +00:00
return
for stream in streams.values():
stream.add_status(instance)