Merge branch 'main' into readthrough-dates
This commit is contained in:
commit
57ef1522b5
26 changed files with 916 additions and 652 deletions
|
@ -275,6 +275,8 @@ def resolve_remote_id(
|
|||
):
|
||||
"""take a remote_id and return an instance, creating if necessary"""
|
||||
if model: # a bonus check we can do if we already know the model
|
||||
if isinstance(model, str):
|
||||
model = apps.get_model(f"bookwyrm.{model}", require_ready=True)
|
||||
result = model.find_existing_by_remote_id(remote_id)
|
||||
if result and not refresh:
|
||||
return result if not get_activity else result.to_activity_dataclass()
|
||||
|
|
|
@ -30,8 +30,8 @@ class Note(ActivityObject):
|
|||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
replies: Dict = field(default_factory=lambda: {})
|
||||
inReplyTo: str = ""
|
||||
summary: str = ""
|
||||
inReplyTo: str = None
|
||||
summary: str = None
|
||||
tag: List[Link] = field(default_factory=lambda: [])
|
||||
attachment: List[Document] = field(default_factory=lambda: [])
|
||||
sensitive: bool = False
|
||||
|
|
49
bookwyrm/migrations/0086_auto_20210828_1724.py
Normal file
49
bookwyrm/migrations/0086_auto_20210828_1724.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
# Generated by Django 3.2.4 on 2021-08-28 17:24
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F, Value, CharField
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
"""generate followers url"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
apps.get_model("bookwyrm", "User").objects.using(db_alias).annotate(
|
||||
generated_url=Concat(
|
||||
F("remote_id"), Value("/followers"), output_field=CharField()
|
||||
)
|
||||
).update(followers_url=models.F("generated_url"))
|
||||
|
||||
|
||||
def reverse_func(apps, schema_editor):
|
||||
"""noop"""
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0085_user_saved_lists"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="followers_url",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
default="/followers", max_length=255
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(forwards_func, reverse_func),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="followers",
|
||||
field=models.ManyToManyField(
|
||||
related_name="following",
|
||||
through="bookwyrm.UserFollows",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -224,8 +224,20 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
|||
original = getattr(instance, self.name)
|
||||
to = data.to
|
||||
cc = data.cc
|
||||
|
||||
# we need to figure out who this is to get their followers link
|
||||
for field in ["attributedTo", "owner", "actor"]:
|
||||
if hasattr(data, field):
|
||||
user_field = field
|
||||
break
|
||||
if not user_field:
|
||||
raise ValidationError("No user field found for privacy", data)
|
||||
user = activitypub.resolve_remote_id(getattr(data, user_field), model="User")
|
||||
|
||||
if to == [self.public]:
|
||||
setattr(instance, self.name, "public")
|
||||
elif to == [user.followers_url]:
|
||||
setattr(instance, self.name, "followers")
|
||||
elif cc == []:
|
||||
setattr(instance, self.name, "direct")
|
||||
elif self.public in cc:
|
||||
|
@ -241,9 +253,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
|||
mentions = [u.remote_id for u in instance.mention_users.all()]
|
||||
# this is a link to the followers list
|
||||
# pylint: disable=protected-access
|
||||
followers = instance.user.__class__._meta.get_field(
|
||||
"followers"
|
||||
).field_to_activity(instance.user.followers)
|
||||
followers = instance.user.followers_url
|
||||
if instance.privacy == "public":
|
||||
activity["to"] = [self.public]
|
||||
activity["cc"] = [followers] + mentions
|
||||
|
|
|
@ -82,9 +82,9 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
preview_image = models.ImageField(
|
||||
upload_to="previews/avatars/", blank=True, null=True
|
||||
)
|
||||
followers = fields.ManyToManyField(
|
||||
followers_url = fields.CharField(max_length=255, activitypub_field="followers")
|
||||
followers = models.ManyToManyField(
|
||||
"self",
|
||||
link_only=True,
|
||||
symmetrical=False,
|
||||
through="UserFollows",
|
||||
through_fields=("user_object", "user_subject"),
|
||||
|
@ -228,7 +228,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
|
||||
def to_followers_activity(self, **kwargs):
|
||||
"""activitypub followers list"""
|
||||
remote_id = "%s/followers" % self.remote_id
|
||||
remote_id = self.followers_url
|
||||
return self.to_ordered_collection(
|
||||
self.followers.order_by("-updated_date").all(),
|
||||
remote_id=remote_id,
|
||||
|
@ -275,10 +275,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
return
|
||||
|
||||
# populate fields for local users
|
||||
self.remote_id = "%s/user/%s" % (site_link(), self.localname)
|
||||
self.inbox = "%s/inbox" % self.remote_id
|
||||
self.shared_inbox = "%s/inbox" % site_link()
|
||||
self.outbox = "%s/outbox" % self.remote_id
|
||||
link = site_link()
|
||||
self.remote_id = f"{link}/user/{self.localname}"
|
||||
self.followers_url = f"{self.remote_id}/followers"
|
||||
self.inbox = f"{self.remote_id}/inbox"
|
||||
self.shared_inbox = f"{link}/inbox"
|
||||
self.outbox = f"{self.remote_id}/outbox"
|
||||
|
||||
# an id needs to be set before we can proceed with related models
|
||||
super().save(*args, **kwargs)
|
||||
|
|
|
@ -109,17 +109,17 @@
|
|||
{% trans 'Settings' %}
|
||||
</a>
|
||||
</li>
|
||||
{% if perms.bookwyrm.create_invites or perms.moderate_users %}
|
||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
||||
<li class="navbar-divider" role="presentation"></li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.create_invites %}
|
||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
||||
<li>
|
||||
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
||||
{% trans 'Invites' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.moderate_users %}
|
||||
{% if perms.bookwyrm.moderate_user %}
|
||||
<li>
|
||||
<a href="{% url 'settings-users' %}" class="navbar-item">
|
||||
{% trans 'Admin' %}
|
||||
|
|
|
@ -21,23 +21,29 @@
|
|||
{% if perms.bookwyrm.create_invites %}
|
||||
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
|
||||
<ul class="menu-list">
|
||||
{% if perms.bookwyrm.moderate_user %}
|
||||
<li>
|
||||
{% url 'settings-users' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Users" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
{% url 'settings-invite-requests' as url %}
|
||||
{% url 'settings-invites' as alt_url %}
|
||||
<a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
|
||||
</li>
|
||||
{% if perms.bookwyrm.moderate_user %}
|
||||
<li>
|
||||
{% url 'settings-reports' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.control_federation %}
|
||||
<li>
|
||||
{% url 'settings-federation' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Instances" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.edit_instance_settings %}
|
||||
|
|
|
@ -25,3 +25,4 @@ class Person(TestCase):
|
|||
self.assertEqual(user.username, "mouse@example.com")
|
||||
self.assertEqual(user.remote_id, "https://example.com/user/mouse")
|
||||
self.assertFalse(user.local)
|
||||
self.assertEqual(user.followers_url, "https://example.com/user/mouse/followers")
|
||||
|
|
|
@ -146,6 +146,15 @@ class ModelFields(TestCase):
|
|||
def test_privacy_field_set_field_from_activity(self, _):
|
||||
"""translate between to/cc fields and privacy"""
|
||||
|
||||
with patch("bookwyrm.models.user.set_remote_server.delay"):
|
||||
test_user = User.objects.create_user(
|
||||
username="test_user@example.com",
|
||||
local=False,
|
||||
remote_id="https://example.com/test_user",
|
||||
inbox="https://example.com/users/test_user/inbox",
|
||||
followers_url="https://example.com/users/test_user/followers",
|
||||
)
|
||||
|
||||
@dataclass(init=False)
|
||||
class TestActivity(ActivityObject):
|
||||
"""real simple mock"""
|
||||
|
@ -154,6 +163,7 @@ class ModelFields(TestCase):
|
|||
cc: List[str]
|
||||
id: str = "http://hi.com"
|
||||
type: str = "Test"
|
||||
attributedTo: str = test_user.remote_id
|
||||
|
||||
class TestPrivacyModel(ActivitypubMixin, BookWyrmModel):
|
||||
"""real simple mock model because BookWyrmModel is abstract"""
|
||||
|
@ -185,6 +195,16 @@ class ModelFields(TestCase):
|
|||
instance.set_field_from_activity(model_instance, data)
|
||||
self.assertEqual(model_instance.privacy_field, "unlisted")
|
||||
|
||||
data.to = [test_user.followers_url]
|
||||
data.cc = []
|
||||
instance.set_field_from_activity(model_instance, data)
|
||||
self.assertEqual(model_instance.privacy_field, "followers")
|
||||
|
||||
data.to = ["http://user_remote/followers"]
|
||||
data.cc = ["http://mentioned_user/remote_id"]
|
||||
instance.set_field_from_activity(model_instance, data)
|
||||
self.assertEqual(model_instance.privacy_field, "followers")
|
||||
|
||||
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
|
||||
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
||||
def test_privacy_field_set_activity_from_field(self, *_):
|
||||
|
|
|
@ -5,11 +5,13 @@ from django.test import TestCase
|
|||
import responses
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.settings import USE_HTTPS, DOMAIN
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
# pylint: disable=missing-function-docstring
|
||||
class User(TestCase):
|
||||
protocol = "https://" if USE_HTTPS else "http://"
|
||||
|
||||
def setUp(self):
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
|
||||
self.user = models.User.objects.create_user(
|
||||
|
@ -24,13 +26,14 @@ class User(TestCase):
|
|||
|
||||
def test_computed_fields(self):
|
||||
"""username instead of id here"""
|
||||
expected_id = "https://%s/user/mouse" % DOMAIN
|
||||
expected_id = f"{self.protocol}{DOMAIN}/user/mouse"
|
||||
self.assertEqual(self.user.remote_id, expected_id)
|
||||
self.assertEqual(self.user.username, "mouse@%s" % DOMAIN)
|
||||
self.assertEqual(self.user.username, f"mouse@{DOMAIN}")
|
||||
self.assertEqual(self.user.localname, "mouse")
|
||||
self.assertEqual(self.user.shared_inbox, "https://%s/inbox" % DOMAIN)
|
||||
self.assertEqual(self.user.inbox, "%s/inbox" % expected_id)
|
||||
self.assertEqual(self.user.outbox, "%s/outbox" % expected_id)
|
||||
self.assertEqual(self.user.shared_inbox, f"{self.protocol}{DOMAIN}/inbox")
|
||||
self.assertEqual(self.user.inbox, f"{expected_id}/inbox")
|
||||
self.assertEqual(self.user.outbox, f"{expected_id}/outbox")
|
||||
self.assertEqual(self.user.followers_url, f"{expected_id}/followers")
|
||||
self.assertIsNotNone(self.user.key_pair.private_key)
|
||||
self.assertIsNotNone(self.user.key_pair.public_key)
|
||||
|
||||
|
|
|
@ -82,6 +82,27 @@ class FeedViews(TestCase):
|
|||
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_status_page_not_found_wrong_user(self, *_):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Status.as_view()
|
||||
another_user = models.User.objects.create_user(
|
||||
"rat@local.com",
|
||||
"rat@rat.rat",
|
||||
"password",
|
||||
local=True,
|
||||
localname="rat",
|
||||
)
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
status = models.Status.objects.create(content="hi", user=another_user)
|
||||
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.feed.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, "mouse", status.id)
|
||||
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_status_page_with_image(self, *_):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Status.as_view()
|
||||
|
|
|
@ -51,6 +51,16 @@ class UserViews(TestCase):
|
|||
data = json.loads(result.getvalue())
|
||||
self.assertEqual(data["subject"], "acct:mouse@local.com")
|
||||
|
||||
def test_webfinger_case_sensitivty(self):
|
||||
"""ensure that webfinger queries are not case sensitive"""
|
||||
request = self.factory.get("", {"resource": "acct:MoUsE@local.com"})
|
||||
request.user = self.anonymous_user
|
||||
|
||||
result = views.webfinger(request)
|
||||
self.assertIsInstance(result, JsonResponse)
|
||||
data = json.loads(result.getvalue())
|
||||
self.assertEqual(data["subject"], "acct:mouse@local.com")
|
||||
|
||||
def test_nodeinfo_pointer(self):
|
||||
"""just tells you where nodeinfo is"""
|
||||
request = self.factory.get("")
|
||||
|
|
|
@ -96,15 +96,11 @@ class Status(View):
|
|||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
status = models.Status.objects.select_subclasses().get(
|
||||
id=status_id, deleted=False
|
||||
user=user, id=status_id, deleted=False
|
||||
)
|
||||
except (ValueError, models.Status.DoesNotExist):
|
||||
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):
|
||||
return HttpResponseNotFound()
|
||||
|
|
|
@ -13,7 +13,7 @@ from bookwyrm.settings import PAGE_LENGTH
|
|||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||
permission_required("bookwyrm.moderate_user", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class UserAdminList(View):
|
||||
|
|
|
@ -20,7 +20,7 @@ def webfinger(request):
|
|||
|
||||
username = resource.replace("acct:", "")
|
||||
try:
|
||||
user = models.User.objects.get(username=username)
|
||||
user = models.User.objects.get(username__iexact=username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound("No account found")
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue