Merge branch 'main' into httpy

This commit is contained in:
Jamie Bliss 2024-01-10 23:02:45 -05:00 committed by GitHub
commit 0f033832d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 116 additions and 12 deletions

View file

@ -583,7 +583,7 @@ class Post(StatorModel):
domain=domain,
fetch=True,
)
if identity is not None:
if identity is not None and not identity.deleted:
mentions.add(identity)
return mentions
@ -765,6 +765,9 @@ class Post(StatorModel):
targets = set()
for mention in self.mentions.all():
targets.add(mention)
if self.visibility in [Post.Visibilities.public, Post.Visibilities.unlisted]:
for interaction in self.interactions.all():
targets.add(interaction.identity)
# Then, if it's not mentions only, also deliver to followers and all hashtag followers
if self.visibility != Post.Visibilities.mentioned:
for follower in self.author.inbound_follows.filter(

View file

@ -248,7 +248,7 @@ class HttpSignature:
body: dict | None,
private_key: str,
key_id: str,
content_type: str = "application/json",
content_type: str = "application/activity+json",
method: Literal["get", "post"] = "post",
timeout: TimeoutTypes = settings.SETUP.REMOTE_TIMEOUT,
):

View file

@ -3,6 +3,36 @@ import pytest
from users.models import Domain
def test_valid_domain():
"""
Tests that a valid domain is valid
"""
assert Domain.is_valid_domain("example.com")
assert Domain.is_valid_domain("xn----gtbspbbmkef.xn--p1ai")
assert Domain.is_valid_domain("underscore_subdomain.example.com")
assert Domain.is_valid_domain("something.versicherung")
assert Domain.is_valid_domain("11.com")
assert Domain.is_valid_domain("a.cn")
assert Domain.is_valid_domain("sub1.sub2.sample.co.uk")
assert Domain.is_valid_domain("somerandomexample.xn--fiqs8s")
assert not Domain.is_valid_domain("über.com")
assert not Domain.is_valid_domain("example.com:4444")
assert not Domain.is_valid_domain("example.-com")
assert not Domain.is_valid_domain("foo@bar.com")
assert not Domain.is_valid_domain("example.")
assert not Domain.is_valid_domain("example.com.")
assert not Domain.is_valid_domain("-example.com")
assert not Domain.is_valid_domain("_example.com")
assert not Domain.is_valid_domain("_example._com")
assert not Domain.is_valid_domain("example_.com")
assert not Domain.is_valid_domain("example")
assert not Domain.is_valid_domain("a......b.com")
assert not Domain.is_valid_domain("a.123")
assert not Domain.is_valid_domain("123.123")
assert not Domain.is_valid_domain("123.123.123.123")
@pytest.mark.django_db
def test_recursive_block():
"""

View file

@ -9,6 +9,7 @@ from django.db import migrations, models
import core.snowflake
import core.uploads
import stator.models
import users.models.domain
import users.models.follow
import users.models.identity
import users.models.inbox_message
@ -58,7 +59,12 @@ class Migration(migrations.Migration):
fields=[
(
"domain",
models.CharField(max_length=250, primary_key=True, serialize=False),
models.CharField(
max_length=250,
primary_key=True,
serialize=False,
validators=[users.models.domain._domain_validator],
),
),
(
"service_domain",

View file

@ -1,5 +1,6 @@
import json
import logging
import re
import ssl
from functools import cached_property
from typing import Optional
@ -8,6 +9,7 @@ import httpx
import pydantic
import urlman
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from core.models import Config
@ -53,6 +55,14 @@ class DomainStates(StateGraph):
return cls.outdated
def _domain_validator(value: str):
if not Domain.is_valid_domain(value):
raise ValidationError(
"%(value)s is not a valid domain",
params={"value": value},
)
class Domain(StatorModel):
"""
Represents a domain that a user can have an account on.
@ -71,7 +81,9 @@ class Domain(StatorModel):
display domains for now, until we start doing better probing.
"""
domain = models.CharField(max_length=250, primary_key=True)
domain = models.CharField(
max_length=250, primary_key=True, validators=[_domain_validator]
)
service_domain = models.CharField(
max_length=250,
null=True,
@ -119,6 +131,19 @@ class Domain(StatorModel):
class Meta:
indexes: list = []
@classmethod
def is_valid_domain(cls, domain: str) -> bool:
"""
Check if a domain is valid, domain must be lowercase
"""
return (
re.match(
r"^(?:[a-z0-9](?:[a-z0-9-_]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-_]{0,61}[a-z]$",
domain,
)
is not None
)
@classmethod
def get_remote_domain(cls, domain: str) -> "Domain":
return cls.objects.get_or_create(domain=domain.lower(), local=False)[0]

View file

@ -81,7 +81,9 @@ class FollowStates(StateGraph):
except httpx.RequestError:
return
return cls.pending_approval
# local/remote follow local, check manually_approve
# local/remote follow local, check deleted & manually_approve
if instance.target.deleted:
return cls.rejecting
if instance.target.manually_approves_followers:
from activities.models import TimelineEvent
@ -280,7 +282,7 @@ class Follow(StatorModel):
"""
return {
"type": "Accept",
"id": self.uri + "#accept",
"id": f"{self.target.actor_uri}follow/{self.id}/#accept",
"actor": self.target.actor_uri,
"object": self.to_ap(),
}
@ -291,7 +293,7 @@ class Follow(StatorModel):
"""
return {
"type": "Reject",
"id": self.uri + "#reject",
"id": f"{self.target.actor_uri}follow/{self.id}/#reject",
"actor": self.target.actor_uri,
"object": self.to_ap(),
}

View file

@ -120,12 +120,47 @@ class IdentityStates(StateGraph):
@classmethod
def handle_deleted(cls, instance: "Identity"):
from activities.models import FanOut
from activities.models import (
FanOut,
Post,
PostInteraction,
PostInteractionStates,
PostStates,
TimelineEvent,
)
from users.models import Bookmark, Follow, FollowStates, HashtagFollow, Report
if not instance.local:
return cls.updated
# Delete local data
TimelineEvent.objects.filter(identity=instance).delete()
Bookmark.objects.filter(identity=instance).delete()
HashtagFollow.objects.filter(identity=instance).delete()
Report.objects.filter(source_identity=instance).delete()
# Nullify all fields and fanout
instance.name = ""
instance.summary = ""
instance.metadata = []
instance.aliases = []
instance.icon_uri = ""
instance.discoverable = False
instance.image.delete(save=False)
instance.icon.delete(save=False)
instance.save()
cls.targets_fan_out(instance, FanOut.Types.identity_edited)
# Delete all posts and interactions
Post.transition_perform_queryset(instance.posts, PostStates.deleted)
PostInteraction.transition_perform_queryset(
instance.interactions, PostInteractionStates.undone
)
# Fanout the deletion and unfollow from both directions
cls.targets_fan_out(instance, FanOut.Types.identity_deleted)
for follower in Follow.objects.filter(target=instance):
follower.transition_perform(FollowStates.rejecting)
for following in Follow.objects.filter(source=instance):
following.transition_perform(FollowStates.undone)
return cls.deleted_fanned_out
@classmethod
@ -677,10 +712,11 @@ class Identity(StatorModel):
"""
Marks the identity and all of its related content as deleted.
"""
# Move all posts to deleted
from activities.models.post import Post, PostStates
from api.models import Authorization, Token
Post.transition_perform_queryset(self.posts, PostStates.deleted)
# Remove all login tokens
Authorization.objects.filter(identity=self).delete()
Token.objects.filter(identity=self).delete()
# Remove all users from ourselves and mark deletion date
self.users.set([])
self.deleted = timezone.now()

View file

@ -172,7 +172,7 @@ class Report(StatorModel):
subject_post=subject_post,
source_domain=Domain.get_remote_domain(domain_id),
type="remote",
complaint=data.get("content"),
complaint=str(data.get("content", "")),
)
def to_ap(self):

View file

@ -18,6 +18,8 @@ def by_handle_or_404(request, handle, local=True, fetch=False) -> Identity:
domain = domain_instance.domain
else:
username, domain = handle.split("@", 1)
if not Domain.is_valid_domain(domain):
raise Http404("Invalid domain")
# Resolve the domain to the display domain
domain_instance = Domain.get_domain(domain)
if domain_instance is None: