1
0
Fork 0

Merge branch 'main' into readthrough-dates

This commit is contained in:
Mouse Reeve 2021-08-29 11:18:35 -07:00
commit 57ef1522b5
26 changed files with 916 additions and 652 deletions

View file

@ -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()

View file

@ -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

View 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,
),
),
]

View file

@ -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

View file

@ -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)

View file

@ -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' %}

View file

@ -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 %}

View file

@ -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")

View file

@ -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, *_):

View file

@ -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)

View file

@ -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()

View file

@ -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("")

View file

@ -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()

View file

@ -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):

View file

@ -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")