mirror of
https://github.com/jointakahe/takahe.git
synced 2024-06-02 21:39:28 +00:00
Merge branch 'main' into httpy
This commit is contained in:
commit
0f033832d6
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
):
|
||||
|
|
|
@ -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():
|
||||
"""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue