mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-04 23:36:32 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
c33eacaf3d
23 changed files with 328 additions and 135 deletions
2
.github/workflows/black.yml
vendored
2
.github/workflows/black.yml
vendored
|
@ -8,6 +8,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
- uses: psf/black@20.8b1
|
- uses: psf/black@stable
|
||||||
with:
|
with:
|
||||||
args: ". --check -l 80 -S"
|
args: ". --check -l 80 -S"
|
||||||
|
|
|
@ -265,7 +265,8 @@ def resolve_remote_id(
|
||||||
"Could not connect to host for remote_id in: %s" % (remote_id)
|
"Could not connect to host for remote_id in: %s" % (remote_id)
|
||||||
)
|
)
|
||||||
# determine the model implicitly, if not provided
|
# determine the model implicitly, if not provided
|
||||||
if not model:
|
# or if it's a model with subclasses like Status, check again
|
||||||
|
if not model or hasattr(model.objects, "select_subclasses"):
|
||||||
model = get_model_from_type(data.get("type"))
|
model = get_model_from_type(data.get("type"))
|
||||||
|
|
||||||
# check for existing items with shared unique identifiers
|
# check for existing items with shared unique identifiers
|
||||||
|
|
|
@ -23,6 +23,7 @@ class Person(ActivityObject):
|
||||||
inbox: str
|
inbox: str
|
||||||
publicKey: PublicKey
|
publicKey: PublicKey
|
||||||
followers: str = None
|
followers: str = None
|
||||||
|
following: str = None
|
||||||
outbox: str = None
|
outbox: str = None
|
||||||
endpoints: Dict = None
|
endpoints: Dict = None
|
||||||
name: str = None
|
name: str = None
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
""" access the activity streams stored in redis """
|
""" access the activity streams stored in redis """
|
||||||
from abc import ABC
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models import signals, Q
|
from django.db.models import signals, Q
|
||||||
import redis
|
|
||||||
|
|
||||||
from bookwyrm import models, settings
|
from bookwyrm import models
|
||||||
|
from bookwyrm.redis_store import RedisStore, r
|
||||||
from bookwyrm.views.helpers import privacy_filter
|
from bookwyrm.views.helpers import privacy_filter
|
||||||
|
|
||||||
r = redis.Redis(
|
|
||||||
host=settings.REDIS_ACTIVITY_HOST,
|
|
||||||
port=settings.REDIS_ACTIVITY_PORT,
|
|
||||||
password=settings.REDIS_ACTIVITY_PASSWORD,
|
|
||||||
db=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
class ActivityStream(RedisStore):
|
||||||
class ActivityStream(ABC):
|
|
||||||
""" a category of activity stream (like home, local, federated) """
|
""" a category of activity stream (like home, local, federated) """
|
||||||
|
|
||||||
def stream_id(self, user):
|
def stream_id(self, user):
|
||||||
|
@ -26,58 +18,40 @@ class ActivityStream(ABC):
|
||||||
""" the redis key for this user's unread count for this stream """
|
""" the redis key for this user's unread count for this stream """
|
||||||
return "{}-unread".format(self.stream_id(user))
|
return "{}-unread".format(self.stream_id(user))
|
||||||
|
|
||||||
def get_value(self, status): # pylint: disable=no-self-use
|
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||||
""" the status id and the rank (ie, published date) """
|
""" statuses are sorted by date published """
|
||||||
return {status.id: status.published_date.timestamp()}
|
return obj.published_date.timestamp()
|
||||||
|
|
||||||
def add_status(self, status):
|
def add_status(self, status):
|
||||||
""" add a status to users' feeds """
|
""" add a status to users' feeds """
|
||||||
value = self.get_value(status)
|
# the pipeline contains all the add-to-stream activities
|
||||||
# we want to do this as a bulk operation, hence "pipeline"
|
pipeline = self.add_object_to_related_stores(status, execute=False)
|
||||||
pipeline = r.pipeline()
|
|
||||||
for user in self.stream_users(status):
|
for user in self.get_audience(status):
|
||||||
# add the status to the feed
|
|
||||||
pipeline.zadd(self.stream_id(user), value)
|
|
||||||
pipeline.zremrangebyrank(
|
|
||||||
self.stream_id(user), 0, -1 * settings.MAX_STREAM_LENGTH
|
|
||||||
)
|
|
||||||
# add to the unread status count
|
# add to the unread status count
|
||||||
pipeline.incr(self.unread_id(user))
|
pipeline.incr(self.unread_id(user))
|
||||||
# and go!
|
|
||||||
pipeline.execute()
|
|
||||||
|
|
||||||
def remove_status(self, status):
|
# and go!
|
||||||
""" remove a status from all feeds """
|
|
||||||
pipeline = r.pipeline()
|
|
||||||
for user in self.stream_users(status):
|
|
||||||
pipeline.zrem(self.stream_id(user), -1, status.id)
|
|
||||||
pipeline.execute()
|
pipeline.execute()
|
||||||
|
|
||||||
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 """
|
||||||
pipeline = r.pipeline()
|
# only add the statuses that the viewer should be able to see (ie, not dms)
|
||||||
statuses = user.status_set.all()[: settings.MAX_STREAM_LENGTH]
|
statuses = privacy_filter(viewer, user.status_set.all())
|
||||||
for status in statuses:
|
self.bulk_add_objects_to_store(statuses, self.stream_id(viewer))
|
||||||
pipeline.zadd(self.stream_id(viewer), self.get_value(status))
|
|
||||||
if statuses:
|
|
||||||
pipeline.zremrangebyrank(
|
|
||||||
self.stream_id(user), 0, -1 * settings.MAX_STREAM_LENGTH
|
|
||||||
)
|
|
||||||
pipeline.execute()
|
|
||||||
|
|
||||||
def remove_user_statuses(self, viewer, user):
|
def remove_user_statuses(self, viewer, user):
|
||||||
""" remove a user's status from another user's feed """
|
""" remove a user's status from another user's feed """
|
||||||
pipeline = r.pipeline()
|
# remove all so that followers only statuses are removed
|
||||||
for status in user.status_set.all()[: settings.MAX_STREAM_LENGTH]:
|
statuses = user.status_set.all()
|
||||||
pipeline.lrem(self.stream_id(viewer), -1, status.id)
|
self.bulk_remove_objects_from_store(statuses, self.stream_id(viewer))
|
||||||
pipeline.execute()
|
|
||||||
|
|
||||||
def get_activity_stream(self, user):
|
def get_activity_stream(self, user):
|
||||||
""" load the ids for statuses to be displayed """
|
""" load the statuses to be displayed """
|
||||||
# clear unreads for this feed
|
# clear unreads for this feed
|
||||||
r.set(self.unread_id(user), 0)
|
r.set(self.unread_id(user), 0)
|
||||||
|
|
||||||
statuses = r.zrevrange(self.stream_id(user), 0, -1)
|
statuses = self.get_store(self.stream_id(user))
|
||||||
return (
|
return (
|
||||||
models.Status.objects.select_subclasses()
|
models.Status.objects.select_subclasses()
|
||||||
.filter(id__in=statuses)
|
.filter(id__in=statuses)
|
||||||
|
@ -88,23 +62,11 @@ class ActivityStream(ABC):
|
||||||
""" get the unread status count for this user's feed """
|
""" get the unread status count for this user's feed """
|
||||||
return int(r.get(self.unread_id(user)) or 0)
|
return int(r.get(self.unread_id(user)) or 0)
|
||||||
|
|
||||||
def populate_stream(self, user):
|
def populate_streams(self, user):
|
||||||
""" go from zero to a timeline """
|
""" go from zero to a timeline """
|
||||||
pipeline = r.pipeline()
|
self.populate_store(self.stream_id(user))
|
||||||
statuses = self.stream_statuses(user)
|
|
||||||
|
|
||||||
stream_id = self.stream_id(user)
|
def get_audience(self, status): # pylint: disable=no-self-use
|
||||||
for status in statuses.all()[: settings.MAX_STREAM_LENGTH]:
|
|
||||||
pipeline.zadd(stream_id, self.get_value(status))
|
|
||||||
|
|
||||||
# only trim the stream if statuses were added
|
|
||||||
if statuses.exists():
|
|
||||||
pipeline.zremrangebyrank(
|
|
||||||
self.stream_id(user), 0, -1 * settings.MAX_STREAM_LENGTH
|
|
||||||
)
|
|
||||||
pipeline.execute()
|
|
||||||
|
|
||||||
def stream_users(self, status): # pylint: disable=no-self-use
|
|
||||||
""" given a status, what users should see it """
|
""" given a status, what users should see it """
|
||||||
# direct messages don't appeard in feeds, direct comments/reviews/etc do
|
# direct messages don't appeard in feeds, direct comments/reviews/etc do
|
||||||
if status.privacy == "direct" and status.status_type == "Note":
|
if status.privacy == "direct" and status.status_type == "Note":
|
||||||
|
@ -132,7 +94,10 @@ class ActivityStream(ABC):
|
||||||
)
|
)
|
||||||
return audience.distinct()
|
return audience.distinct()
|
||||||
|
|
||||||
def stream_statuses(self, user): # pylint: disable=no-self-use
|
def get_stores_for_object(self, obj):
|
||||||
|
return [self.stream_id(u) for u in self.get_audience(obj)]
|
||||||
|
|
||||||
|
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 privacy_filter(
|
||||||
user,
|
user,
|
||||||
|
@ -140,14 +105,18 @@ class ActivityStream(ABC):
|
||||||
privacy_levels=["public", "unlisted", "followers"],
|
privacy_levels=["public", "unlisted", "followers"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_objects_for_store(self, store):
|
||||||
|
user = models.User.objects.get(id=store.split("-")[0])
|
||||||
|
return self.get_statuses_for_user(user)
|
||||||
|
|
||||||
|
|
||||||
class HomeStream(ActivityStream):
|
class HomeStream(ActivityStream):
|
||||||
""" users you follow """
|
""" users you follow """
|
||||||
|
|
||||||
key = "home"
|
key = "home"
|
||||||
|
|
||||||
def stream_users(self, status):
|
def get_audience(self, status):
|
||||||
audience = super().stream_users(status)
|
audience = super().get_audience(status)
|
||||||
if not audience:
|
if not audience:
|
||||||
return []
|
return []
|
||||||
return audience.filter(
|
return audience.filter(
|
||||||
|
@ -155,7 +124,7 @@ class HomeStream(ActivityStream):
|
||||||
| Q(following=status.user) # if the user is following the author
|
| Q(following=status.user) # if the user is following the author
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def stream_statuses(self, user):
|
def get_statuses_for_user(self, user):
|
||||||
return privacy_filter(
|
return privacy_filter(
|
||||||
user,
|
user,
|
||||||
models.Status.objects.select_subclasses(),
|
models.Status.objects.select_subclasses(),
|
||||||
|
@ -169,13 +138,13 @@ class LocalStream(ActivityStream):
|
||||||
|
|
||||||
key = "local"
|
key = "local"
|
||||||
|
|
||||||
def stream_users(self, status):
|
def get_audience(self, status):
|
||||||
# this stream wants no part in non-public statuses
|
# this stream wants no part in non-public statuses
|
||||||
if status.privacy != "public" or not status.user.local:
|
if status.privacy != "public" or not status.user.local:
|
||||||
return []
|
return []
|
||||||
return super().stream_users(status)
|
return super().get_audience(status)
|
||||||
|
|
||||||
def stream_statuses(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 privacy_filter(
|
||||||
user,
|
user,
|
||||||
|
@ -189,13 +158,13 @@ class FederatedStream(ActivityStream):
|
||||||
|
|
||||||
key = "federated"
|
key = "federated"
|
||||||
|
|
||||||
def stream_users(self, status):
|
def get_audience(self, status):
|
||||||
# this stream wants no part in non-public statuses
|
# this stream wants no part in non-public statuses
|
||||||
if status.privacy != "public":
|
if status.privacy != "public":
|
||||||
return []
|
return []
|
||||||
return super().stream_users(status)
|
return super().get_audience(status)
|
||||||
|
|
||||||
def stream_statuses(self, user):
|
def get_statuses_for_user(self, user):
|
||||||
return privacy_filter(
|
return privacy_filter(
|
||||||
user,
|
user,
|
||||||
models.Status.objects.select_subclasses(),
|
models.Status.objects.select_subclasses(),
|
||||||
|
@ -220,7 +189,7 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
|
||||||
|
|
||||||
if instance.deleted:
|
if instance.deleted:
|
||||||
for stream in streams.values():
|
for stream in streams.values():
|
||||||
stream.remove_status(instance)
|
stream.remove_object_from_related_stores(instance)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -237,7 +206,7 @@ def remove_boost_on_delete(sender, instance, *args, **kwargs):
|
||||||
""" boosts are deleted """
|
""" boosts are deleted """
|
||||||
# we're only interested in new statuses
|
# we're only interested in new statuses
|
||||||
for stream in streams.values():
|
for stream in streams.values():
|
||||||
stream.remove_status(instance)
|
stream.remove_object_from_related_stores(instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_save, sender=models.UserFollows)
|
@receiver(signals.post_save, sender=models.UserFollows)
|
||||||
|
@ -297,4 +266,4 @@ def populate_streams_on_account_create(sender, instance, created, *args, **kwarg
|
||||||
return
|
return
|
||||||
|
|
||||||
for stream in streams.values():
|
for stream in streams.values():
|
||||||
stream.populate_stream(instance)
|
stream.populate_streams(instance)
|
||||||
|
|
|
@ -179,7 +179,11 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
|
|
||||||
mapped_data = dict_from_mappings(data, self.author_mappings)
|
mapped_data = dict_from_mappings(data, self.author_mappings)
|
||||||
activity = activitypub.Author(**mapped_data)
|
try:
|
||||||
|
activity = activitypub.Author(**mapped_data)
|
||||||
|
except activitypub.ActivitySerializerError:
|
||||||
|
return None
|
||||||
|
|
||||||
# this will dedupe
|
# this will dedupe
|
||||||
return activity.to_model(model=models.Author)
|
return activity.to_model(model=models.Author)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" interface with whatever connectors the app has """
|
""" interface with whatever connectors the app has """
|
||||||
import importlib
|
import importlib
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
@ -11,6 +12,8 @@ from requests import HTTPError
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ConnectorException(HTTPError):
|
class ConnectorException(HTTPError):
|
||||||
""" when the connector can't do what was asked """
|
""" when the connector can't do what was asked """
|
||||||
|
@ -44,7 +47,9 @@ def search(query, min_confidence=0.1):
|
||||||
if result_set in (None, []):
|
if result_set in (None, []):
|
||||||
try:
|
try:
|
||||||
result_set = connector.search(query, min_confidence=min_confidence)
|
result_set = connector.search(query, min_confidence=min_confidence)
|
||||||
except (HTTPError, ConnectorException):
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
# we don't want *any* error to crash the whole search page
|
||||||
|
logger.exception(e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# if the search results look the same, ignore them
|
# if the search results look the same, ignore them
|
||||||
|
|
|
@ -93,7 +93,10 @@ class Connector(AbstractConnector):
|
||||||
# this id is "/authors/OL1234567A"
|
# this id is "/authors/OL1234567A"
|
||||||
author_id = author_blob["key"]
|
author_id = author_blob["key"]
|
||||||
url = "%s%s" % (self.base_url, author_id)
|
url = "%s%s" % (self.base_url, author_id)
|
||||||
yield self.get_or_create_author(url)
|
author = self.get_or_create_author(url)
|
||||||
|
if not author:
|
||||||
|
continue
|
||||||
|
yield author
|
||||||
|
|
||||||
def get_cover_url(self, cover_blob, size="L"):
|
def get_cover_url(self, cover_blob, size="L"):
|
||||||
""" ask openlibrary for the cover """
|
""" ask openlibrary for the cover """
|
||||||
|
|
33
bookwyrm/migrations/0062_auto_20210407_1545.py
Normal file
33
bookwyrm/migrations/0062_auto_20210407_1545.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 3.1.6 on 2021-04-07 15:45
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0061_auto_20210402_1435"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="series",
|
||||||
|
field=bookwyrm.models.fields.TextField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="subtitle",
|
||||||
|
field=bookwyrm.models.fields.TextField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="title",
|
||||||
|
field=bookwyrm.models.fields.TextField(max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,6 @@
|
||||||
""" activitypub model functionality """
|
""" activitypub model functionality """
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
from collections import namedtuple
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import json
|
import json
|
||||||
import operator
|
import operator
|
||||||
|
@ -25,6 +26,15 @@ from bookwyrm.models.fields import ImageField, ManyToManyField
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
# I tried to separate these classes into mutliple files but I kept getting
|
# I tried to separate these classes into mutliple files but I kept getting
|
||||||
# circular import errors so I gave up. I'm sure it could be done though!
|
# circular import errors so I gave up. I'm sure it could be done though!
|
||||||
|
|
||||||
|
PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
|
||||||
|
|
||||||
|
|
||||||
|
def set_activity_from_property_field(activity, obj, field):
|
||||||
|
""" assign a model property value to the activity json """
|
||||||
|
activity[field[1]] = getattr(obj, field[0])
|
||||||
|
|
||||||
|
|
||||||
class ActivitypubMixin:
|
class ActivitypubMixin:
|
||||||
""" add this mixin for models that are AP serializable """
|
""" add this mixin for models that are AP serializable """
|
||||||
|
|
||||||
|
@ -52,6 +62,11 @@ class ActivitypubMixin:
|
||||||
self.activity_fields = (
|
self.activity_fields = (
|
||||||
self.image_fields + self.many_to_many_fields + self.simple_fields
|
self.image_fields + self.many_to_many_fields + self.simple_fields
|
||||||
)
|
)
|
||||||
|
if hasattr(self, "property_fields"):
|
||||||
|
self.activity_fields += [
|
||||||
|
PropertyField(lambda a, o: set_activity_from_property_field(a, o, f))
|
||||||
|
for f in self.property_fields
|
||||||
|
]
|
||||||
|
|
||||||
# these are separate to avoid infinite recursion issues
|
# these are separate to avoid infinite recursion issues
|
||||||
self.deserialize_reverse_fields = (
|
self.deserialize_reverse_fields = (
|
||||||
|
@ -430,7 +445,7 @@ def generate_activity(obj):
|
||||||
) in obj.serialize_reverse_fields:
|
) in obj.serialize_reverse_fields:
|
||||||
related_field = getattr(obj, model_field_name)
|
related_field = getattr(obj, model_field_name)
|
||||||
activity[activity_field_name] = unfurl_related_field(
|
activity[activity_field_name] = unfurl_related_field(
|
||||||
related_field, sort_field
|
related_field, sort_field=sort_field
|
||||||
)
|
)
|
||||||
|
|
||||||
if not activity.get("id"):
|
if not activity.get("id"):
|
||||||
|
@ -440,7 +455,7 @@ def generate_activity(obj):
|
||||||
|
|
||||||
def unfurl_related_field(related_field, sort_field=None):
|
def unfurl_related_field(related_field, sort_field=None):
|
||||||
""" load reverse lookups (like public key owner or Status attachment """
|
""" load reverse lookups (like public key owner or Status attachment """
|
||||||
if hasattr(related_field, "all"):
|
if sort_field and hasattr(related_field, "all"):
|
||||||
return [
|
return [
|
||||||
unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
|
unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
|
||||||
]
|
]
|
||||||
|
|
|
@ -53,14 +53,14 @@ class Book(BookDataModel):
|
||||||
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
|
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
|
||||||
|
|
||||||
# book/work metadata
|
# book/work metadata
|
||||||
title = fields.CharField(max_length=255)
|
title = fields.TextField(max_length=255)
|
||||||
sort_title = fields.CharField(max_length=255, blank=True, null=True)
|
sort_title = fields.CharField(max_length=255, blank=True, null=True)
|
||||||
subtitle = fields.CharField(max_length=255, blank=True, null=True)
|
subtitle = fields.TextField(max_length=255, blank=True, null=True)
|
||||||
description = fields.HtmlField(blank=True, null=True)
|
description = fields.HtmlField(blank=True, null=True)
|
||||||
languages = fields.ArrayField(
|
languages = fields.ArrayField(
|
||||||
models.CharField(max_length=255), blank=True, default=list
|
models.CharField(max_length=255), blank=True, default=list
|
||||||
)
|
)
|
||||||
series = fields.CharField(max_length=255, blank=True, null=True)
|
series = fields.TextField(max_length=255, blank=True, null=True)
|
||||||
series_number = fields.CharField(max_length=255, blank=True, null=True)
|
series_number = fields.CharField(max_length=255, blank=True, null=True)
|
||||||
subjects = fields.ArrayField(
|
subjects = fields.ArrayField(
|
||||||
models.CharField(max_length=255), blank=True, null=True, default=list
|
models.CharField(max_length=255), blank=True, null=True, default=list
|
||||||
|
|
|
@ -112,6 +112,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
)
|
)
|
||||||
|
|
||||||
name_field = "username"
|
name_field = "username"
|
||||||
|
property_fields = [("following_link", "following")]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def following_link(self):
|
||||||
|
""" just how to find out the following info """
|
||||||
|
return "{:s}/following".format(self.remote_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alt_text(self):
|
def alt_text(self):
|
||||||
|
|
89
bookwyrm/redis_store.py
Normal file
89
bookwyrm/redis_store.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
""" access the activity stores stored in redis """
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import redis
|
||||||
|
|
||||||
|
from bookwyrm import settings
|
||||||
|
|
||||||
|
r = redis.Redis(
|
||||||
|
host=settings.REDIS_ACTIVITY_HOST,
|
||||||
|
port=settings.REDIS_ACTIVITY_PORT,
|
||||||
|
password=settings.REDIS_ACTIVITY_PASSWORD,
|
||||||
|
db=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RedisStore(ABC):
|
||||||
|
""" sets of ranked, related objects, like statuses for a user's feed """
|
||||||
|
|
||||||
|
max_length = settings.MAX_STREAM_LENGTH
|
||||||
|
|
||||||
|
def get_value(self, obj):
|
||||||
|
""" the object and rank """
|
||||||
|
return {obj.id: self.get_rank(obj)}
|
||||||
|
|
||||||
|
def add_object_to_related_stores(self, obj, execute=True):
|
||||||
|
""" add an object to all suitable stores """
|
||||||
|
value = self.get_value(obj)
|
||||||
|
# we want to do this as a bulk operation, hence "pipeline"
|
||||||
|
pipeline = r.pipeline()
|
||||||
|
for store in self.get_stores_for_object(obj):
|
||||||
|
# add the status to the feed
|
||||||
|
pipeline.zadd(store, value)
|
||||||
|
# trim the store
|
||||||
|
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
||||||
|
if not execute:
|
||||||
|
return pipeline
|
||||||
|
# and go!
|
||||||
|
return pipeline.execute()
|
||||||
|
|
||||||
|
def remove_object_from_related_stores(self, obj):
|
||||||
|
""" remove an object from all stores """
|
||||||
|
pipeline = r.pipeline()
|
||||||
|
for store in self.get_stores_for_object(obj):
|
||||||
|
pipeline.zrem(store, -1, obj.id)
|
||||||
|
pipeline.execute()
|
||||||
|
|
||||||
|
def bulk_add_objects_to_store(self, objs, store):
|
||||||
|
""" add a list of objects to a given store """
|
||||||
|
pipeline = r.pipeline()
|
||||||
|
for obj in objs[: self.max_length]:
|
||||||
|
pipeline.zadd(store, self.get_value(obj))
|
||||||
|
if objs:
|
||||||
|
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
||||||
|
pipeline.execute()
|
||||||
|
|
||||||
|
def bulk_remove_objects_from_store(self, objs, store):
|
||||||
|
""" remoev a list of objects from a given store """
|
||||||
|
pipeline = r.pipeline()
|
||||||
|
for obj in objs[: self.max_length]:
|
||||||
|
pipeline.zrem(store, -1, obj.id)
|
||||||
|
pipeline.execute()
|
||||||
|
|
||||||
|
def get_store(self, store): # pylint: disable=no-self-use
|
||||||
|
""" load the values in a store """
|
||||||
|
return r.zrevrange(store, 0, -1)
|
||||||
|
|
||||||
|
def populate_store(self, store):
|
||||||
|
""" go from zero to a store """
|
||||||
|
pipeline = r.pipeline()
|
||||||
|
queryset = self.get_objects_for_store(store)
|
||||||
|
|
||||||
|
for obj in queryset[: self.max_length]:
|
||||||
|
pipeline.zadd(store, self.get_value(obj))
|
||||||
|
|
||||||
|
# only trim the store if objects were added
|
||||||
|
if queryset.exists():
|
||||||
|
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
||||||
|
pipeline.execute()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_objects_for_store(self, store):
|
||||||
|
""" a queryset of what should go in a store, used for populating it """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_stores_for_object(self, obj):
|
||||||
|
""" the stores that an object belongs in """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_rank(self, obj):
|
||||||
|
""" how to rank an object """
|
|
@ -88,12 +88,18 @@
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
||||||
<p class="mb-2"><label class="label" for="id_title">{% trans "Title:" %}</label> {{ form.title }} </p>
|
<p class="mb-2">
|
||||||
|
<label class="label" for="id_title">{% trans "Title:" %}</label>
|
||||||
|
<input type="text" name="title" value="{{ form.title.value }}" maxlength="255" class="input" required="" id="id_title">
|
||||||
|
</p>
|
||||||
{% for error in form.title.errors %}
|
{% for error in form.title.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p class="mb-2"><label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label> {{ form.subtitle }} </p>
|
<p class="mb-2">
|
||||||
|
<label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label>
|
||||||
|
<input type="text" name="subtitle" value="{{ form.subtitle.value }}" maxlength="255" class="input" required="" id="id_subtitle">
|
||||||
|
</p>
|
||||||
{% for error in form.subtitle.errors %}
|
{% for error in form.subtitle.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a
|
<a
|
||||||
href="{{ user.local_path }}"
|
href="{{ request.user.local_path }}"
|
||||||
class="navbar-link pulldown-menu"
|
class="navbar-link pulldown-menu"
|
||||||
role="button"
|
role="button"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
|
|
|
@ -116,7 +116,9 @@ class Status(TestCase):
|
||||||
|
|
||||||
def test_status_to_activity_tombstone(self, *_):
|
def test_status_to_activity_tombstone(self, *_):
|
||||||
""" subclass of the base model version with a "pure" serializer """
|
""" subclass of the base model version with a "pure" serializer """
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.remove_status"):
|
with patch(
|
||||||
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
|
):
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
content="test content",
|
content="test content",
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
|
|
|
@ -47,18 +47,18 @@ class Activitystreams(TestCase):
|
||||||
"{}-test-unread".format(self.local_user.id),
|
"{}-test-unread".format(self.local_user.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_abstractstream_stream_users(self, *_):
|
def test_abstractstream_get_audience(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user, content="hi", privacy="public"
|
user=self.remote_user, content="hi", privacy="public"
|
||||||
)
|
)
|
||||||
users = self.test_stream.stream_users(status)
|
users = self.test_stream.get_audience(status)
|
||||||
# remote users don't have feeds
|
# remote users don't have feeds
|
||||||
self.assertFalse(self.remote_user in users)
|
self.assertFalse(self.remote_user in users)
|
||||||
self.assertTrue(self.local_user in users)
|
self.assertTrue(self.local_user in users)
|
||||||
self.assertTrue(self.another_user in users)
|
self.assertTrue(self.another_user in users)
|
||||||
|
|
||||||
def test_abstractstream_stream_users_direct(self, *_):
|
def test_abstractstream_get_audience_direct(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user,
|
user=self.remote_user,
|
||||||
|
@ -66,7 +66,7 @@ class Activitystreams(TestCase):
|
||||||
privacy="direct",
|
privacy="direct",
|
||||||
)
|
)
|
||||||
status.mention_users.add(self.local_user)
|
status.mention_users.add(self.local_user)
|
||||||
users = self.test_stream.stream_users(status)
|
users = self.test_stream.get_audience(status)
|
||||||
self.assertEqual(users, [])
|
self.assertEqual(users, [])
|
||||||
|
|
||||||
status = models.Comment.objects.create(
|
status = models.Comment.objects.create(
|
||||||
|
@ -76,22 +76,22 @@ class Activitystreams(TestCase):
|
||||||
book=self.book,
|
book=self.book,
|
||||||
)
|
)
|
||||||
status.mention_users.add(self.local_user)
|
status.mention_users.add(self.local_user)
|
||||||
users = self.test_stream.stream_users(status)
|
users = self.test_stream.get_audience(status)
|
||||||
self.assertTrue(self.local_user in users)
|
self.assertTrue(self.local_user in users)
|
||||||
self.assertFalse(self.another_user in users)
|
self.assertFalse(self.another_user in users)
|
||||||
self.assertFalse(self.remote_user in users)
|
self.assertFalse(self.remote_user in users)
|
||||||
|
|
||||||
def test_abstractstream_stream_users_followers_remote_user(self, *_):
|
def test_abstractstream_get_audience_followers_remote_user(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user,
|
user=self.remote_user,
|
||||||
content="hi",
|
content="hi",
|
||||||
privacy="followers",
|
privacy="followers",
|
||||||
)
|
)
|
||||||
users = self.test_stream.stream_users(status)
|
users = self.test_stream.get_audience(status)
|
||||||
self.assertFalse(users.exists())
|
self.assertFalse(users.exists())
|
||||||
|
|
||||||
def test_abstractstream_stream_users_followers_self(self, *_):
|
def test_abstractstream_get_audience_followers_self(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Comment.objects.create(
|
status = models.Comment.objects.create(
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
|
@ -99,12 +99,12 @@ class Activitystreams(TestCase):
|
||||||
privacy="direct",
|
privacy="direct",
|
||||||
book=self.book,
|
book=self.book,
|
||||||
)
|
)
|
||||||
users = self.test_stream.stream_users(status)
|
users = self.test_stream.get_audience(status)
|
||||||
self.assertTrue(self.local_user in users)
|
self.assertTrue(self.local_user in users)
|
||||||
self.assertFalse(self.another_user in users)
|
self.assertFalse(self.another_user in users)
|
||||||
self.assertFalse(self.remote_user in users)
|
self.assertFalse(self.remote_user in users)
|
||||||
|
|
||||||
def test_abstractstream_stream_users_followers_with_mention(self, *_):
|
def test_abstractstream_get_audience_followers_with_mention(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Comment.objects.create(
|
status = models.Comment.objects.create(
|
||||||
user=self.remote_user,
|
user=self.remote_user,
|
||||||
|
@ -114,12 +114,12 @@ class Activitystreams(TestCase):
|
||||||
)
|
)
|
||||||
status.mention_users.add(self.local_user)
|
status.mention_users.add(self.local_user)
|
||||||
|
|
||||||
users = self.test_stream.stream_users(status)
|
users = self.test_stream.get_audience(status)
|
||||||
self.assertTrue(self.local_user in users)
|
self.assertTrue(self.local_user in users)
|
||||||
self.assertFalse(self.another_user in users)
|
self.assertFalse(self.another_user in users)
|
||||||
self.assertFalse(self.remote_user in users)
|
self.assertFalse(self.remote_user in users)
|
||||||
|
|
||||||
def test_abstractstream_stream_users_followers_with_relationship(self, *_):
|
def test_abstractstream_get_audience_followers_with_relationship(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
self.remote_user.followers.add(self.local_user)
|
self.remote_user.followers.add(self.local_user)
|
||||||
status = models.Comment.objects.create(
|
status = models.Comment.objects.create(
|
||||||
|
@ -128,77 +128,77 @@ class Activitystreams(TestCase):
|
||||||
privacy="direct",
|
privacy="direct",
|
||||||
book=self.book,
|
book=self.book,
|
||||||
)
|
)
|
||||||
users = self.test_stream.stream_users(status)
|
users = self.test_stream.get_audience(status)
|
||||||
self.assertFalse(self.local_user in users)
|
self.assertFalse(self.local_user in users)
|
||||||
self.assertFalse(self.another_user in users)
|
self.assertFalse(self.another_user in users)
|
||||||
self.assertFalse(self.remote_user in users)
|
self.assertFalse(self.remote_user in users)
|
||||||
|
|
||||||
def test_homestream_stream_users(self, *_):
|
def test_homestream_get_audience(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user, content="hi", privacy="public"
|
user=self.remote_user, content="hi", privacy="public"
|
||||||
)
|
)
|
||||||
users = activitystreams.HomeStream().stream_users(status)
|
users = activitystreams.HomeStream().get_audience(status)
|
||||||
self.assertFalse(users.exists())
|
self.assertFalse(users.exists())
|
||||||
|
|
||||||
def test_homestream_stream_users_with_mentions(self, *_):
|
def test_homestream_get_audience_with_mentions(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user, content="hi", privacy="public"
|
user=self.remote_user, content="hi", privacy="public"
|
||||||
)
|
)
|
||||||
status.mention_users.add(self.local_user)
|
status.mention_users.add(self.local_user)
|
||||||
users = activitystreams.HomeStream().stream_users(status)
|
users = activitystreams.HomeStream().get_audience(status)
|
||||||
self.assertFalse(self.local_user in users)
|
self.assertFalse(self.local_user in users)
|
||||||
self.assertFalse(self.another_user in users)
|
self.assertFalse(self.another_user in users)
|
||||||
|
|
||||||
def test_homestream_stream_users_with_relationship(self, *_):
|
def test_homestream_get_audience_with_relationship(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
self.remote_user.followers.add(self.local_user)
|
self.remote_user.followers.add(self.local_user)
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user, content="hi", privacy="public"
|
user=self.remote_user, content="hi", privacy="public"
|
||||||
)
|
)
|
||||||
users = activitystreams.HomeStream().stream_users(status)
|
users = activitystreams.HomeStream().get_audience(status)
|
||||||
self.assertTrue(self.local_user in users)
|
self.assertTrue(self.local_user in users)
|
||||||
self.assertFalse(self.another_user in users)
|
self.assertFalse(self.another_user in users)
|
||||||
|
|
||||||
def test_localstream_stream_users_remote_status(self, *_):
|
def test_localstream_get_audience_remote_status(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user, content="hi", privacy="public"
|
user=self.remote_user, content="hi", privacy="public"
|
||||||
)
|
)
|
||||||
users = activitystreams.LocalStream().stream_users(status)
|
users = activitystreams.LocalStream().get_audience(status)
|
||||||
self.assertEqual(users, [])
|
self.assertEqual(users, [])
|
||||||
|
|
||||||
def test_localstream_stream_users_local_status(self, *_):
|
def test_localstream_get_audience_local_status(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.local_user, content="hi", privacy="public"
|
user=self.local_user, content="hi", privacy="public"
|
||||||
)
|
)
|
||||||
users = activitystreams.LocalStream().stream_users(status)
|
users = activitystreams.LocalStream().get_audience(status)
|
||||||
self.assertTrue(self.local_user in users)
|
self.assertTrue(self.local_user in users)
|
||||||
self.assertTrue(self.another_user in users)
|
self.assertTrue(self.another_user in users)
|
||||||
|
|
||||||
def test_localstream_stream_users_unlisted(self, *_):
|
def test_localstream_get_audience_unlisted(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.local_user, content="hi", privacy="unlisted"
|
user=self.local_user, content="hi", privacy="unlisted"
|
||||||
)
|
)
|
||||||
users = activitystreams.LocalStream().stream_users(status)
|
users = activitystreams.LocalStream().get_audience(status)
|
||||||
self.assertEqual(users, [])
|
self.assertEqual(users, [])
|
||||||
|
|
||||||
def test_federatedstream_stream_users(self, *_):
|
def test_federatedstream_get_audience(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user, content="hi", privacy="public"
|
user=self.remote_user, content="hi", privacy="public"
|
||||||
)
|
)
|
||||||
users = activitystreams.FederatedStream().stream_users(status)
|
users = activitystreams.FederatedStream().get_audience(status)
|
||||||
self.assertTrue(self.local_user in users)
|
self.assertTrue(self.local_user in users)
|
||||||
self.assertTrue(self.another_user in users)
|
self.assertTrue(self.another_user in users)
|
||||||
|
|
||||||
def test_federatedstream_stream_users_unlisted(self, *_):
|
def test_federatedstream_get_audience_unlisted(self, *_):
|
||||||
""" get a list of users that should see a status """
|
""" get a list of users that should see a status """
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
user=self.remote_user, content="hi", privacy="unlisted"
|
user=self.remote_user, content="hi", privacy="unlisted"
|
||||||
)
|
)
|
||||||
users = activitystreams.FederatedStream().stream_users(status)
|
users = activitystreams.FederatedStream().get_audience(status)
|
||||||
self.assertEqual(users, [])
|
self.assertEqual(users, [])
|
||||||
|
|
|
@ -85,7 +85,9 @@ class TemplateTags(TestCase):
|
||||||
second_child = models.Status.objects.create(
|
second_child = models.Status.objects.create(
|
||||||
reply_parent=parent, user=self.user, content="hi"
|
reply_parent=parent, user=self.user, content="hi"
|
||||||
)
|
)
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.remove_status"):
|
with patch(
|
||||||
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
|
):
|
||||||
third_child = models.Status.objects.create(
|
third_child = models.Status.objects.create(
|
||||||
reply_parent=parent,
|
reply_parent=parent,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
|
|
@ -444,7 +444,7 @@ class Inbox(TestCase):
|
||||||
"object": {"id": self.status.remote_id, "type": "Tombstone"},
|
"object": {"id": self.status.remote_id, "type": "Tombstone"},
|
||||||
}
|
}
|
||||||
with patch(
|
with patch(
|
||||||
"bookwyrm.activitystreams.ActivityStream.remove_status"
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
) as redis_mock:
|
) as redis_mock:
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
self.assertTrue(redis_mock.called)
|
self.assertTrue(redis_mock.called)
|
||||||
|
@ -477,7 +477,7 @@ class Inbox(TestCase):
|
||||||
"object": {"id": self.status.remote_id, "type": "Tombstone"},
|
"object": {"id": self.status.remote_id, "type": "Tombstone"},
|
||||||
}
|
}
|
||||||
with patch(
|
with patch(
|
||||||
"bookwyrm.activitystreams.ActivityStream.remove_status"
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
) as redis_mock:
|
) as redis_mock:
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
self.assertTrue(redis_mock.called)
|
self.assertTrue(redis_mock.called)
|
||||||
|
@ -572,6 +572,56 @@ class Inbox(TestCase):
|
||||||
self.assertEqual(notification.user, self.local_user)
|
self.assertEqual(notification.user, self.local_user)
|
||||||
self.assertEqual(notification.related_status, self.status)
|
self.assertEqual(notification.related_status, self.status)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
||||||
|
def test_handle_boost_remote_status(self, redis_mock):
|
||||||
|
""" boost a status """
|
||||||
|
work = models.Work.objects.create(title="work title")
|
||||||
|
book = models.Edition.objects.create(
|
||||||
|
title="Test",
|
||||||
|
remote_id="https://bookwyrm.social/book/37292",
|
||||||
|
parent_work=work,
|
||||||
|
)
|
||||||
|
self.assertEqual(models.Notification.objects.count(), 0)
|
||||||
|
activity = {
|
||||||
|
"type": "Announce",
|
||||||
|
"id": "%s/boost" % self.status.remote_id,
|
||||||
|
"actor": self.remote_user.remote_id,
|
||||||
|
"object": "https://remote.com/status/1",
|
||||||
|
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||||
|
"cc": ["https://example.com/user/mouse/followers"],
|
||||||
|
"published": "Mon, 25 May 2020 19:31:20 GMT",
|
||||||
|
}
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
"https://remote.com/status/1",
|
||||||
|
json={
|
||||||
|
"id": "https://remote.com/status/1",
|
||||||
|
"type": "Comment",
|
||||||
|
"published": "2021-04-05T18:04:59.735190+00:00",
|
||||||
|
"attributedTo": self.remote_user.remote_id,
|
||||||
|
"content": "<p>a comment</p>",
|
||||||
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc": ["https://b875df3d118b.ngrok.io/user/mouse/followers"],
|
||||||
|
"inReplyTo": "",
|
||||||
|
"inReplyToBook": book.remote_id,
|
||||||
|
"summary": "",
|
||||||
|
"tag": [],
|
||||||
|
"sensitive": False,
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
|
||||||
|
discarder.return_value = False
|
||||||
|
views.inbox.activity_task(activity)
|
||||||
|
self.assertTrue(redis_mock.called)
|
||||||
|
|
||||||
|
boost = models.Boost.objects.get()
|
||||||
|
self.assertEqual(boost.boosted_status.remote_id, "https://remote.com/status/1")
|
||||||
|
self.assertEqual(boost.boosted_status.comment.status_type, "Comment")
|
||||||
|
self.assertEqual(boost.boosted_status.comment.book, book)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_handle_discarded_boost(self):
|
def test_handle_discarded_boost(self):
|
||||||
""" test a boost of a mastodon status that will be discarded """
|
""" test a boost of a mastodon status that will be discarded """
|
||||||
|
@ -616,7 +666,7 @@ class Inbox(TestCase):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
with patch(
|
with patch(
|
||||||
"bookwyrm.activitystreams.ActivityStream.remove_status"
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
) as redis_mock:
|
) as redis_mock:
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
self.assertTrue(redis_mock.called)
|
self.assertTrue(redis_mock.called)
|
||||||
|
|
|
@ -164,7 +164,7 @@ class InteractionViews(TestCase):
|
||||||
self.assertEqual(models.Boost.objects.count(), 1)
|
self.assertEqual(models.Boost.objects.count(), 1)
|
||||||
self.assertEqual(models.Notification.objects.count(), 1)
|
self.assertEqual(models.Notification.objects.count(), 1)
|
||||||
with patch(
|
with patch(
|
||||||
"bookwyrm.activitystreams.ActivityStream.remove_status"
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
) as redis_mock:
|
) as redis_mock:
|
||||||
view(request, status.id)
|
view(request, status.id)
|
||||||
self.assertTrue(redis_mock.called)
|
self.assertTrue(redis_mock.called)
|
||||||
|
|
|
@ -177,7 +177,9 @@ class StatusViews(TestCase):
|
||||||
content="hi", book=self.book, user=self.local_user
|
content="hi", book=self.book, user=self.local_user
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock:
|
with patch(
|
||||||
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
|
) as mock:
|
||||||
result = view(request, status.id)
|
result = view(request, status.id)
|
||||||
self.assertTrue(mock.called)
|
self.assertTrue(mock.called)
|
||||||
result.render()
|
result.render()
|
||||||
|
@ -196,7 +198,9 @@ class StatusViews(TestCase):
|
||||||
book=self.book, rating=2.0, user=self.local_user
|
book=self.book, rating=2.0, user=self.local_user
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock:
|
with patch(
|
||||||
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
|
) as mock:
|
||||||
result = view(request, status.id)
|
result = view(request, status.id)
|
||||||
self.assertFalse(mock.called)
|
self.assertFalse(mock.called)
|
||||||
self.assertEqual(result.status_code, 400)
|
self.assertEqual(result.status_code, 400)
|
||||||
|
@ -214,7 +218,9 @@ class StatusViews(TestCase):
|
||||||
content="hi", user=self.local_user
|
content="hi", user=self.local_user
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock:
|
with patch(
|
||||||
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
|
) as mock:
|
||||||
result = view(request, status.id)
|
result = view(request, status.id)
|
||||||
self.assertFalse(mock.called)
|
self.assertFalse(mock.called)
|
||||||
self.assertEqual(result.status_code, 400)
|
self.assertEqual(result.status_code, 400)
|
||||||
|
@ -316,7 +322,7 @@ class StatusViews(TestCase):
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"bookwyrm.activitystreams.ActivityStream.remove_status"
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
) as redis_mock:
|
) as redis_mock:
|
||||||
view(request, status.id)
|
view(request, status.id)
|
||||||
self.assertTrue(redis_mock.called)
|
self.assertTrue(redis_mock.called)
|
||||||
|
@ -351,7 +357,7 @@ class StatusViews(TestCase):
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"bookwyrm.activitystreams.ActivityStream.remove_status"
|
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
|
||||||
) as redis_mock:
|
) as redis_mock:
|
||||||
view(request, status.id)
|
view(request, status.id)
|
||||||
self.assertTrue(redis_mock.called)
|
self.assertTrue(redis_mock.called)
|
||||||
|
|
|
@ -31,7 +31,6 @@ class Feed(View):
|
||||||
tab = "home"
|
tab = "home"
|
||||||
|
|
||||||
activities = activitystreams.streams[tab].get_activity_stream(request.user)
|
activities = activitystreams.streams[tab].get_activity_stream(request.user)
|
||||||
|
|
||||||
paginated = Paginator(activities, PAGE_LENGTH)
|
paginated = Paginator(activities, PAGE_LENGTH)
|
||||||
|
|
||||||
suggested_users = get_suggested_users(request.user)
|
suggested_users = get_suggested_users(request.user)
|
||||||
|
|
|
@ -138,7 +138,7 @@ def handle_remote_webfinger(query):
|
||||||
user = activitypub.resolve_remote_id(
|
user = activitypub.resolve_remote_id(
|
||||||
link["href"], model=models.User
|
link["href"], model=models.User
|
||||||
)
|
)
|
||||||
except KeyError:
|
except (KeyError, activitypub.ActivitySerializerError):
|
||||||
return None
|
return None
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
16
bw-dev
16
bw-dev
|
@ -14,6 +14,9 @@ trap - EXIT
|
||||||
|
|
||||||
# show commands as they're executed
|
# show commands as they're executed
|
||||||
set -x
|
set -x
|
||||||
|
function runweb {
|
||||||
|
docker-compose run --rm web "$@"
|
||||||
|
}
|
||||||
|
|
||||||
function execdb {
|
function execdb {
|
||||||
docker-compose exec db $@
|
docker-compose exec db $@
|
||||||
|
@ -45,17 +48,16 @@ case "$CMD" in
|
||||||
initdb
|
initdb
|
||||||
;;
|
;;
|
||||||
makemigrations)
|
makemigrations)
|
||||||
execweb python manage.py makemigrations "$@"
|
runweb python manage.py makemigrations "$@"
|
||||||
;;
|
;;
|
||||||
migrate)
|
migrate)
|
||||||
execweb python manage.py rename_app fedireads bookwyrm
|
runweb python manage.py migrate "$@"
|
||||||
execweb python manage.py migrate "$@"
|
|
||||||
;;
|
;;
|
||||||
bash)
|
bash)
|
||||||
execweb bash
|
runweb bash
|
||||||
;;
|
;;
|
||||||
shell)
|
shell)
|
||||||
execweb python manage.py shell
|
runweb python manage.py shell
|
||||||
;;
|
;;
|
||||||
dbshell)
|
dbshell)
|
||||||
execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
|
execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
|
||||||
|
@ -64,7 +66,7 @@ case "$CMD" in
|
||||||
docker-compose restart celery_worker
|
docker-compose restart celery_worker
|
||||||
;;
|
;;
|
||||||
collectstatic)
|
collectstatic)
|
||||||
execweb python manage.py collectstatic --no-input
|
runweb python manage.py collectstatic --no-input
|
||||||
;;
|
;;
|
||||||
build)
|
build)
|
||||||
docker-compose build
|
docker-compose build
|
||||||
|
@ -77,7 +79,7 @@ case "$CMD" in
|
||||||
docker-compose restart
|
docker-compose restart
|
||||||
;;
|
;;
|
||||||
populate_streams)
|
populate_streams)
|
||||||
execweb python manage.py populate_streams
|
runweb python manage.py populate_streams
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unrecognised command. Try: build, up, initdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, update, populate_feeds"
|
echo "Unrecognised command. Try: build, up, initdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, update, populate_feeds"
|
||||||
|
|
Loading…
Reference in a new issue