Remove all remaining async code for now
This commit is contained in:
parent
0915b17c4b
commit
188e5a2446
|
@ -4,7 +4,6 @@ from typing import ClassVar
|
|||
|
||||
import httpx
|
||||
import urlman
|
||||
from asgiref.sync import sync_to_async
|
||||
from cachetools import TTLCache, cached
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
|
@ -35,13 +34,13 @@ class EmojiStates(StateGraph):
|
|||
outdated.transitions_to(updated)
|
||||
|
||||
@classmethod
|
||||
async def handle_outdated(cls, instance: "Emoji"):
|
||||
def handle_outdated(cls, instance: "Emoji"):
|
||||
"""
|
||||
Fetches remote emoji and uploads to file for local caching
|
||||
"""
|
||||
if instance.remote_url and not instance.file:
|
||||
try:
|
||||
file, mimetype = await get_remote_file(
|
||||
file, mimetype = get_remote_file(
|
||||
instance.remote_url,
|
||||
timeout=settings.SETUP.REMOTE_TIMEOUT,
|
||||
max_size=settings.SETUP.EMOJI_MAX_IMAGE_FILESIZE_KB * 1024,
|
||||
|
@ -55,7 +54,7 @@ class EmojiStates(StateGraph):
|
|||
|
||||
instance.file = file
|
||||
instance.mimetype = mimetype
|
||||
await sync_to_async(instance.save)()
|
||||
instance.save()
|
||||
|
||||
return cls.updated
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import httpx
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.db import models
|
||||
|
||||
from activities.models.timeline_event import TimelineEvent
|
||||
|
@ -77,7 +76,7 @@ class FanOutStates(StateGraph):
|
|||
post = instance.subject_post
|
||||
# Sign it and send it
|
||||
try:
|
||||
async_to_sync(post.author.signed_request)(
|
||||
post.author.signed_request(
|
||||
method="post",
|
||||
uri=(
|
||||
instance.identity.shared_inbox_uri
|
||||
|
@ -93,7 +92,7 @@ class FanOutStates(StateGraph):
|
|||
post = instance.subject_post
|
||||
# Sign it and send it
|
||||
try:
|
||||
async_to_sync(post.author.signed_request)(
|
||||
post.author.signed_request(
|
||||
method="post",
|
||||
uri=(
|
||||
instance.identity.shared_inbox_uri
|
||||
|
@ -119,7 +118,7 @@ class FanOutStates(StateGraph):
|
|||
post = instance.subject_post
|
||||
# Send it to the remote inbox
|
||||
try:
|
||||
async_to_sync(post.author.signed_request)(
|
||||
post.author.signed_request(
|
||||
method="post",
|
||||
uri=(
|
||||
instance.identity.shared_inbox_uri
|
||||
|
@ -172,7 +171,7 @@ class FanOutStates(StateGraph):
|
|||
body = interaction.to_add_ap()
|
||||
else:
|
||||
body = interaction.to_create_ap()
|
||||
async_to_sync(interaction.identity.signed_request)(
|
||||
interaction.identity.signed_request(
|
||||
method="post",
|
||||
uri=(
|
||||
instance.identity.shared_inbox_uri
|
||||
|
@ -202,7 +201,7 @@ class FanOutStates(StateGraph):
|
|||
body = interaction.to_remove_ap()
|
||||
else:
|
||||
body = interaction.to_undo_ap()
|
||||
async_to_sync(interaction.identity.signed_request)(
|
||||
interaction.identity.signed_request(
|
||||
method="post",
|
||||
uri=(
|
||||
instance.identity.shared_inbox_uri
|
||||
|
@ -217,7 +216,7 @@ class FanOutStates(StateGraph):
|
|||
case (FanOut.Types.identity_edited, False):
|
||||
identity = instance.subject_identity
|
||||
try:
|
||||
async_to_sync(identity.signed_request)(
|
||||
identity.signed_request(
|
||||
method="post",
|
||||
uri=(
|
||||
instance.identity.shared_inbox_uri
|
||||
|
@ -232,7 +231,7 @@ class FanOutStates(StateGraph):
|
|||
case (FanOut.Types.identity_deleted, False):
|
||||
identity = instance.subject_identity
|
||||
try:
|
||||
async_to_sync(identity.signed_request)(
|
||||
identity.signed_request(
|
||||
method="post",
|
||||
uri=(
|
||||
instance.identity.shared_inbox_uri
|
||||
|
|
|
@ -8,7 +8,6 @@ from urllib.parse import urlparse
|
|||
|
||||
import httpx
|
||||
import urlman
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVector
|
||||
from django.db import models, transaction
|
||||
|
@ -831,7 +830,7 @@ class Post(StatorModel):
|
|||
# If the author is not fetched yet, try again later
|
||||
if author.domain is None:
|
||||
if fetch_author:
|
||||
async_to_sync(author.fetch_actor)()
|
||||
author.fetch_actor()
|
||||
# perhaps the entire "try again" logic below
|
||||
# could be replaced with TryAgainLater for
|
||||
# _all_ fetches, to let it handle pinned posts?
|
||||
|
@ -981,7 +980,7 @@ class Post(StatorModel):
|
|||
except cls.DoesNotExist:
|
||||
if fetch:
|
||||
try:
|
||||
response = async_to_sync(SystemActor().signed_request)(
|
||||
response = SystemActor().signed_request(
|
||||
method="get", uri=object_uri
|
||||
)
|
||||
except (httpx.HTTPError, ssl.SSLCertVerificationError):
|
||||
|
@ -1008,7 +1007,7 @@ class Post(StatorModel):
|
|||
) from err
|
||||
# We may need to fetch the author too
|
||||
if post.author.state == IdentityStates.outdated:
|
||||
async_to_sync(post.author.fetch_actor)()
|
||||
post.author.fetch_actor()
|
||||
return post
|
||||
else:
|
||||
raise cls.DoesNotExist(f"Cannot find Post with URI {object_uri}")
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import httpx
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
from activities.models import Hashtag, Post
|
||||
from core.ld import canonicalise
|
||||
|
@ -49,7 +48,7 @@ class SearchService:
|
|||
username, domain_instance or domain, fetch=True
|
||||
)
|
||||
if identity and identity.state == IdentityStates.outdated:
|
||||
async_to_sync(identity.fetch_actor)()
|
||||
identity.fetch_actor()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
@ -74,7 +73,7 @@ class SearchService:
|
|||
|
||||
# Fetch the provided URL as the system actor to retrieve the AP JSON
|
||||
try:
|
||||
response = async_to_sync(SystemActor().signed_request)(
|
||||
response = SystemActor().signed_request(
|
||||
method="get",
|
||||
uri=self.query,
|
||||
)
|
||||
|
@ -90,7 +89,7 @@ class SearchService:
|
|||
# Try and retrieve the profile by actor URI
|
||||
identity = Identity.by_actor_uri(document["id"], create=True)
|
||||
if identity and identity.state == IdentityStates.outdated:
|
||||
async_to_sync(identity.fetch_actor)()
|
||||
identity.fetch_actor()
|
||||
return identity
|
||||
|
||||
# Is it a post?
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import json
|
||||
|
||||
import httpx
|
||||
from asgiref.sync import async_to_sync
|
||||
from django import forms
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
@ -13,7 +12,6 @@ from users.models import SystemActor
|
|||
|
||||
@method_decorator(admin_required, name="dispatch")
|
||||
class JsonViewer(FormView):
|
||||
|
||||
template_name = "activities/debug_json.html"
|
||||
|
||||
class form_class(forms.Form):
|
||||
|
@ -31,7 +29,7 @@ class JsonViewer(FormView):
|
|||
context = self.get_context_data(form=form)
|
||||
|
||||
try:
|
||||
response = async_to_sync(SystemActor().signed_request)(
|
||||
response = SystemActor().signed_request(
|
||||
method="get",
|
||||
uri=uri,
|
||||
)
|
||||
|
@ -64,18 +62,15 @@ class JsonViewer(FormView):
|
|||
|
||||
|
||||
class NotFound(TemplateView):
|
||||
|
||||
template_name = "404.html"
|
||||
|
||||
|
||||
class ServerError(TemplateView):
|
||||
|
||||
template_name = "500.html"
|
||||
|
||||
|
||||
@method_decorator(admin_required, name="dispatch")
|
||||
class OauthAuthorize(TemplateView):
|
||||
|
||||
template_name = "api/oauth_authorize.html"
|
||||
|
||||
def get_context_data(self):
|
||||
|
|
|
@ -57,7 +57,7 @@ def blurhash_image(file) -> str:
|
|||
return blurhash.encode(file, 4, 4)
|
||||
|
||||
|
||||
async def get_remote_file(
|
||||
def get_remote_file(
|
||||
url: str,
|
||||
*,
|
||||
timeout: float = settings.SETUP.REMOTE_TIMEOUT,
|
||||
|
@ -70,8 +70,8 @@ async def get_remote_file(
|
|||
"User-Agent": settings.TAKAHE_USER_AGENT,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
async with client.stream(
|
||||
with httpx.Client(headers=headers) as client:
|
||||
with client.stream(
|
||||
"GET", url, timeout=timeout, follow_redirects=True
|
||||
) as stream:
|
||||
allow_download = max_size is None
|
||||
|
@ -82,7 +82,7 @@ async def get_remote_file(
|
|||
except (KeyError, TypeError):
|
||||
pass
|
||||
if allow_download:
|
||||
file = ContentFile(await stream.aread(), name=url)
|
||||
file = ContentFile(stream.read(), name=url)
|
||||
return file, stream.headers.get(
|
||||
"content-type", "application/octet-stream"
|
||||
)
|
||||
|
|
|
@ -177,7 +177,7 @@ class HttpSignature:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
async def signed_request(
|
||||
def signed_request(
|
||||
cls,
|
||||
uri: str,
|
||||
body: dict | None,
|
||||
|
@ -241,9 +241,9 @@ class HttpSignature:
|
|||
|
||||
# Send the request with all those headers except the pseudo one
|
||||
del headers["(request-target)"]
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
response = client.request(
|
||||
method,
|
||||
uri,
|
||||
headers=headers,
|
||||
|
|
|
@ -238,10 +238,6 @@ def stator(config_system) -> StatorRunner:
|
|||
"""
|
||||
Return an initialized StatorRunner for tests that need state transitioning
|
||||
to happen.
|
||||
|
||||
Example:
|
||||
# Do some tasks with state side effects
|
||||
async_to_sync(stator_runner.fetch_and_process_tasks)()
|
||||
"""
|
||||
runner = StatorRunner(
|
||||
StatorModel.subclasses,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import pytest
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.test.client import RequestFactory
|
||||
from pytest_httpx import HTTPXMock
|
||||
|
||||
|
@ -75,7 +74,7 @@ def test_sign_http(httpx_mock: HTTPXMock, keypair):
|
|||
}
|
||||
# Send the signed request to the mock library
|
||||
httpx_mock.add_response()
|
||||
async_to_sync(HttpSignature.signed_request)(
|
||||
HttpSignature.signed_request(
|
||||
uri="https://example.com/test-actor",
|
||||
body=document,
|
||||
private_key=keypair["private_key"],
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import pytest
|
||||
from asgiref.sync import async_to_sync
|
||||
from pytest_httpx import HTTPXMock
|
||||
|
||||
from core.models import Config
|
||||
|
@ -169,7 +168,7 @@ def test_fetch_actor(httpx_mock, config_system):
|
|||
"url": "https://example.com/test-actor/view/",
|
||||
},
|
||||
)
|
||||
async_to_sync(identity.fetch_actor)()
|
||||
identity.fetch_actor()
|
||||
|
||||
# Verify the data arrived
|
||||
identity = Identity.objects.get(pk=identity.pk)
|
||||
|
@ -189,15 +188,14 @@ def test_fetch_actor(httpx_mock, config_system):
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_webfinger_url(httpx_mock: HTTPXMock, config_system):
|
||||
def test_fetch_webfinger_url(httpx_mock: HTTPXMock, config_system):
|
||||
"""
|
||||
Ensures that we can deal with various kinds of webfinger URLs
|
||||
"""
|
||||
|
||||
# With no host-meta, it should be the default
|
||||
assert (
|
||||
await Identity.fetch_webfinger_url("example.com")
|
||||
Identity.fetch_webfinger_url("example.com")
|
||||
== "https://example.com/.well-known/webfinger?resource={uri}"
|
||||
)
|
||||
|
||||
|
@ -210,7 +208,7 @@ async def test_fetch_webfinger_url(httpx_mock: HTTPXMock, config_system):
|
|||
</XRD>""",
|
||||
)
|
||||
assert (
|
||||
await Identity.fetch_webfinger_url("example.com")
|
||||
Identity.fetch_webfinger_url("example.com")
|
||||
== "https://fedi.example.com/.well-known/webfinger?resource={uri}"
|
||||
)
|
||||
|
||||
|
@ -223,7 +221,7 @@ async def test_fetch_webfinger_url(httpx_mock: HTTPXMock, config_system):
|
|||
</XRD>""",
|
||||
)
|
||||
assert (
|
||||
await Identity.fetch_webfinger_url("example.com")
|
||||
Identity.fetch_webfinger_url("example.com")
|
||||
== "https://example.com/amazing-webfinger?query={uri}"
|
||||
)
|
||||
|
||||
|
@ -237,7 +235,7 @@ async def test_fetch_webfinger_url(httpx_mock: HTTPXMock, config_system):
|
|||
</XRD>""",
|
||||
)
|
||||
assert (
|
||||
await Identity.fetch_webfinger_url("example.com")
|
||||
Identity.fetch_webfinger_url("example.com")
|
||||
== "https://example.com/.well-known/webfinger?resource={uri}"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import pytest
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.test.client import RequestFactory
|
||||
from pytest_httpx import HTTPXMock
|
||||
|
||||
|
@ -16,7 +15,7 @@ def test_system_actor_signed(config_system, httpx_mock: HTTPXMock):
|
|||
system_actor.generate_keys()
|
||||
# Send a fake outbound request
|
||||
httpx_mock.add_response()
|
||||
async_to_sync(system_actor.signed_request)(
|
||||
system_actor.signed_request(
|
||||
method="get",
|
||||
uri="http://example.com/test-actor",
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from asgiref.sync import async_to_sync
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.utils import formats
|
||||
|
@ -60,7 +59,7 @@ class DomainAdmin(admin.ModelAdmin):
|
|||
@admin.action(description="Fetch nodeinfo")
|
||||
def fetch_nodeinfo(self, request, queryset):
|
||||
for instance in queryset:
|
||||
info = async_to_sync(instance.fetch_nodeinfo)()
|
||||
info = instance.fetch_nodeinfo()
|
||||
if info:
|
||||
instance.nodeinfo = info.dict()
|
||||
instance.save()
|
||||
|
|
|
@ -30,7 +30,7 @@ class BlockStates(StateGraph):
|
|||
return [cls.new, cls.sent, cls.awaiting_expiry]
|
||||
|
||||
@classmethod
|
||||
async def handle_new(cls, instance: "Block"):
|
||||
def handle_new(cls, instance: "Block"):
|
||||
"""
|
||||
Block that are new need us to deliver the Block object
|
||||
to the target server.
|
||||
|
@ -38,20 +38,18 @@ class BlockStates(StateGraph):
|
|||
# Mutes don't send but might need expiry
|
||||
if instance.mute:
|
||||
return cls.awaiting_expiry
|
||||
# Fetch more info
|
||||
block = await instance.afetch_full()
|
||||
# Remote blocks should not be here, local blocks just work
|
||||
if not block.source.local or block.target.local:
|
||||
if not instance.source.local or instance.target.local:
|
||||
return cls.sent
|
||||
# Don't try if the other identity didn't fetch yet
|
||||
if not block.target.inbox_uri:
|
||||
if not instance.target.inbox_uri:
|
||||
return
|
||||
# Sign it and send it
|
||||
try:
|
||||
await block.source.signed_request(
|
||||
instance.source.signed_request(
|
||||
method="post",
|
||||
uri=block.target.inbox_uri,
|
||||
body=canonicalise(block.to_ap()),
|
||||
uri=instance.target.inbox_uri,
|
||||
body=canonicalise(instance.to_ap()),
|
||||
)
|
||||
except httpx.RequestError:
|
||||
return
|
||||
|
@ -66,19 +64,18 @@ class BlockStates(StateGraph):
|
|||
return cls.undone
|
||||
|
||||
@classmethod
|
||||
async def handle_undone(cls, instance: "Block"):
|
||||
def handle_undone(cls, instance: "Block"):
|
||||
"""
|
||||
Delivers the Undo object to the target server
|
||||
"""
|
||||
block = await instance.afetch_full()
|
||||
# Remote blocks should not be here, mutes don't send, local blocks just work
|
||||
if not block.source.local or block.target.local or instance.mute:
|
||||
if not instance.source.local or instance.target.local or instance.mute:
|
||||
return cls.undone_sent
|
||||
try:
|
||||
await block.source.signed_request(
|
||||
instance.source.signed_request(
|
||||
method="post",
|
||||
uri=block.target.inbox_uri,
|
||||
body=canonicalise(block.to_undo_ap()),
|
||||
uri=instance.target.inbox_uri,
|
||||
body=canonicalise(instance.to_undo_ap()),
|
||||
)
|
||||
except httpx.RequestError:
|
||||
return
|
||||
|
@ -227,16 +224,6 @@ class Block(StatorModel):
|
|||
def active(self):
|
||||
return self.state in BlockStates.group_active()
|
||||
|
||||
### Async helpers ###
|
||||
|
||||
async def afetch_full(self):
|
||||
"""
|
||||
Returns a version of the object with all relations pre-loaded
|
||||
"""
|
||||
return await Block.objects.select_related(
|
||||
"source", "source__domain", "target"
|
||||
).aget(pk=self.pk)
|
||||
|
||||
### ActivityPub (outbound) ###
|
||||
|
||||
def to_ap(self):
|
||||
|
|
|
@ -6,7 +6,6 @@ from typing import Optional
|
|||
import httpx
|
||||
import pydantic
|
||||
import urlman
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
@ -33,15 +32,15 @@ class DomainStates(StateGraph):
|
|||
outdated.times_out_to(connection_issue, 60 * 60 * 24)
|
||||
|
||||
@classmethod
|
||||
async def handle_outdated(cls, instance: "Domain"):
|
||||
info = await instance.fetch_nodeinfo()
|
||||
def handle_outdated(cls, instance: "Domain"):
|
||||
info = instance.fetch_nodeinfo()
|
||||
if info:
|
||||
instance.nodeinfo = info.dict()
|
||||
await sync_to_async(instance.save)()
|
||||
instance.save()
|
||||
return cls.updated
|
||||
|
||||
@classmethod
|
||||
async def handle_updated(cls, instance: "Domain"):
|
||||
def handle_updated(cls, instance: "Domain"):
|
||||
return cls.outdated
|
||||
|
||||
|
||||
|
@ -157,18 +156,18 @@ class Domain(StatorModel):
|
|||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
async def fetch_nodeinfo(self) -> NodeInfo | None:
|
||||
def fetch_nodeinfo(self) -> NodeInfo | None:
|
||||
"""
|
||||
Fetch the /NodeInfo/2.0 for the domain
|
||||
"""
|
||||
nodeinfo20_url = f"https://{self.domain}/nodeinfo/2.0"
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
with httpx.Client(
|
||||
timeout=settings.SETUP.REMOTE_TIMEOUT,
|
||||
headers={"User-Agent": settings.TAKAHE_USER_AGENT},
|
||||
) as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
response = client.get(
|
||||
f"https://{self.domain}/.well-known/nodeinfo",
|
||||
follow_redirects=True,
|
||||
headers={"Accept": "application/json"},
|
||||
|
@ -190,7 +189,7 @@ class Domain(StatorModel):
|
|||
pass
|
||||
|
||||
try:
|
||||
response = await client.get(
|
||||
response = client.get(
|
||||
nodeinfo20_url,
|
||||
follow_redirects=True,
|
||||
headers={"Accept": "application/json"},
|
||||
|
|
|
@ -34,26 +34,25 @@ class FollowStates(StateGraph):
|
|||
return [cls.unrequested, cls.local_requested, cls.accepted]
|
||||
|
||||
@classmethod
|
||||
async def handle_unrequested(cls, instance: "Follow"):
|
||||
def handle_unrequested(cls, instance: "Follow"):
|
||||
"""
|
||||
Follows that are unrequested need us to deliver the Follow object
|
||||
to the target server.
|
||||
"""
|
||||
follow = await instance.afetch_full()
|
||||
# Remote follows should not be here
|
||||
if not follow.source.local:
|
||||
if not instance.source.local:
|
||||
return cls.remote_requested
|
||||
if follow.target.local:
|
||||
if instance.target.local:
|
||||
return cls.accepted
|
||||
# Don't try if the other identity didn't fetch yet
|
||||
if not follow.target.inbox_uri:
|
||||
if not instance.target.inbox_uri:
|
||||
return
|
||||
# Sign it and send it
|
||||
try:
|
||||
await follow.source.signed_request(
|
||||
instance.source.signed_request(
|
||||
method="post",
|
||||
uri=follow.target.inbox_uri,
|
||||
body=canonicalise(follow.to_ap()),
|
||||
uri=instance.target.inbox_uri,
|
||||
body=canonicalise(instance.to_ap()),
|
||||
)
|
||||
except httpx.RequestError:
|
||||
return
|
||||
|
@ -65,33 +64,31 @@ class FollowStates(StateGraph):
|
|||
pass
|
||||
|
||||
@classmethod
|
||||
async def handle_remote_requested(cls, instance: "Follow"):
|
||||
def handle_remote_requested(cls, instance: "Follow"):
|
||||
"""
|
||||
Items in remote_requested need us to send an Accept object to the
|
||||
source server.
|
||||
"""
|
||||
follow = await instance.afetch_full()
|
||||
try:
|
||||
await follow.target.signed_request(
|
||||
instance.target.signed_request(
|
||||
method="post",
|
||||
uri=follow.source.inbox_uri,
|
||||
body=canonicalise(follow.to_accept_ap()),
|
||||
uri=instance.source.inbox_uri,
|
||||
body=canonicalise(instance.to_accept_ap()),
|
||||
)
|
||||
except httpx.RequestError:
|
||||
return
|
||||
return cls.accepted
|
||||
|
||||
@classmethod
|
||||
async def handle_undone(cls, instance: "Follow"):
|
||||
def handle_undone(cls, instance: "Follow"):
|
||||
"""
|
||||
Delivers the Undo object to the target server
|
||||
"""
|
||||
follow = await instance.afetch_full()
|
||||
try:
|
||||
await follow.source.signed_request(
|
||||
instance.source.signed_request(
|
||||
method="post",
|
||||
uri=follow.target.inbox_uri,
|
||||
body=canonicalise(follow.to_undo_ap()),
|
||||
uri=instance.target.inbox_uri,
|
||||
body=canonicalise(instance.to_undo_ap()),
|
||||
)
|
||||
except httpx.RequestError:
|
||||
return
|
||||
|
@ -204,16 +201,6 @@ class Follow(StatorModel):
|
|||
follow.save()
|
||||
return follow
|
||||
|
||||
### Async helpers ###
|
||||
|
||||
async def afetch_full(self):
|
||||
"""
|
||||
Returns a version of the object with all relations pre-loaded
|
||||
"""
|
||||
return await Follow.objects.select_related(
|
||||
"source", "source__domain", "target"
|
||||
).aget(pk=self.pk)
|
||||
|
||||
### Properties ###
|
||||
|
||||
@property
|
||||
|
|
|
@ -5,7 +5,6 @@ from urllib.parse import urlparse
|
|||
|
||||
import httpx
|
||||
import urlman
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError, models
|
||||
from django.utils import timezone
|
||||
|
@ -66,13 +65,13 @@ class IdentityStates(StateGraph):
|
|||
return [cls.deleted, cls.deleted_fanned_out]
|
||||
|
||||
@classmethod
|
||||
async def targets_fan_out(cls, identity: "Identity", type_: str) -> None:
|
||||
def targets_fan_out(cls, identity: "Identity", type_: str) -> None:
|
||||
from activities.models import FanOut
|
||||
from users.models import Follow
|
||||
|
||||
# Fan out to each target
|
||||
shared_inboxes = set()
|
||||
async for follower in Follow.objects.select_related("source", "target").filter(
|
||||
for follower in Follow.objects.select_related("source", "target").filter(
|
||||
target=identity
|
||||
):
|
||||
# Dedupe shared_inbox_uri
|
||||
|
@ -80,7 +79,7 @@ class IdentityStates(StateGraph):
|
|||
if shared_uri and shared_uri in shared_inboxes:
|
||||
continue
|
||||
|
||||
await FanOut.objects.acreate(
|
||||
FanOut.objects.create(
|
||||
identity=follower.source,
|
||||
type=type_,
|
||||
subject_identity=identity,
|
||||
|
@ -88,34 +87,32 @@ class IdentityStates(StateGraph):
|
|||
shared_inboxes.add(shared_uri)
|
||||
|
||||
@classmethod
|
||||
async def handle_edited(cls, instance: "Identity"):
|
||||
def handle_edited(cls, instance: "Identity"):
|
||||
from activities.models import FanOut
|
||||
|
||||
if not instance.local:
|
||||
return cls.updated
|
||||
|
||||
identity = await instance.afetch_full()
|
||||
await cls.targets_fan_out(identity, FanOut.Types.identity_edited)
|
||||
cls.targets_fan_out(instance, FanOut.Types.identity_edited)
|
||||
return cls.updated
|
||||
|
||||
@classmethod
|
||||
async def handle_deleted(cls, instance: "Identity"):
|
||||
def handle_deleted(cls, instance: "Identity"):
|
||||
from activities.models import FanOut
|
||||
|
||||
if not instance.local:
|
||||
return cls.updated
|
||||
|
||||
identity = await instance.afetch_full()
|
||||
await cls.targets_fan_out(identity, FanOut.Types.identity_deleted)
|
||||
cls.targets_fan_out(instance, FanOut.Types.identity_deleted)
|
||||
return cls.deleted_fanned_out
|
||||
|
||||
@classmethod
|
||||
async def handle_outdated(cls, identity: "Identity"):
|
||||
def handle_outdated(cls, identity: "Identity"):
|
||||
# Local identities never need fetching
|
||||
if identity.local:
|
||||
return cls.updated
|
||||
# Run the actor fetch and progress to updated if it succeeds
|
||||
if await identity.fetch_actor():
|
||||
if identity.fetch_actor():
|
||||
return cls.updated
|
||||
|
||||
@classmethod
|
||||
|
@ -365,9 +362,7 @@ class Identity(StatorModel):
|
|||
)
|
||||
except cls.DoesNotExist:
|
||||
if fetch and not local:
|
||||
actor_uri, handle = async_to_sync(cls.fetch_webfinger)(
|
||||
f"{username}@{domain}"
|
||||
)
|
||||
actor_uri, handle = cls.fetch_webfinger(f"{username}@{domain}")
|
||||
if handle is None:
|
||||
return None
|
||||
# See if this actually does match an existing actor
|
||||
|
@ -449,14 +444,6 @@ class Identity(StatorModel):
|
|||
def limited(self) -> bool:
|
||||
return self.restriction == self.Restriction.limited
|
||||
|
||||
### Async helpers ###
|
||||
|
||||
async def afetch_full(self):
|
||||
"""
|
||||
Returns a version of the object with all relations pre-loaded
|
||||
"""
|
||||
return await Identity.objects.select_related("domain").aget(pk=self.pk)
|
||||
|
||||
### ActivityPub (outbound) ###
|
||||
|
||||
def to_webfinger(self):
|
||||
|
@ -637,17 +624,17 @@ class Identity(StatorModel):
|
|||
### Actor/Webfinger fetching ###
|
||||
|
||||
@classmethod
|
||||
async def fetch_webfinger_url(cls, domain: str):
|
||||
def fetch_webfinger_url(cls, domain: str):
|
||||
"""
|
||||
Given a domain (hostname), returns the correct webfinger URL to use
|
||||
based on probing host-meta.
|
||||
"""
|
||||
async with httpx.AsyncClient(
|
||||
with httpx.Client(
|
||||
timeout=settings.SETUP.REMOTE_TIMEOUT,
|
||||
headers={"User-Agent": settings.TAKAHE_USER_AGENT},
|
||||
) as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
response = client.get(
|
||||
f"https://{domain}/.well-known/host-meta",
|
||||
follow_redirects=True,
|
||||
headers={"Accept": "application/xml"},
|
||||
|
@ -669,24 +656,24 @@ class Identity(StatorModel):
|
|||
return f"https://{domain}/.well-known/webfinger?resource={{uri}}"
|
||||
|
||||
@classmethod
|
||||
async def fetch_webfinger(cls, handle: str) -> tuple[str | None, str | None]:
|
||||
def fetch_webfinger(cls, handle: str) -> tuple[str | None, str | None]:
|
||||
"""
|
||||
Given a username@domain handle, returns a tuple of
|
||||
(actor uri, canonical handle) or None, None if it does not resolve.
|
||||
"""
|
||||
domain = handle.split("@")[1].lower()
|
||||
try:
|
||||
webfinger_url = await cls.fetch_webfinger_url(domain)
|
||||
webfinger_url = cls.fetch_webfinger_url(domain)
|
||||
except ssl.SSLCertVerificationError:
|
||||
return None, None
|
||||
|
||||
# Go make a Webfinger request
|
||||
async with httpx.AsyncClient(
|
||||
with httpx.Client(
|
||||
timeout=settings.SETUP.REMOTE_TIMEOUT,
|
||||
headers={"User-Agent": settings.TAKAHE_USER_AGENT},
|
||||
) as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
response = client.get(
|
||||
webfinger_url.format(uri=f"acct:{handle}"),
|
||||
follow_redirects=True,
|
||||
headers={"Accept": "application/json"},
|
||||
|
@ -730,16 +717,16 @@ class Identity(StatorModel):
|
|||
return None, None
|
||||
|
||||
@classmethod
|
||||
async def fetch_pinned_post_uris(cls, uri: str) -> list[str]:
|
||||
def fetch_pinned_post_uris(cls, uri: str) -> list[str]:
|
||||
"""
|
||||
Fetch an identity's featured collection.
|
||||
"""
|
||||
async with httpx.AsyncClient(
|
||||
with httpx.Client(
|
||||
timeout=settings.SETUP.REMOTE_TIMEOUT,
|
||||
headers={"User-Agent": settings.TAKAHE_USER_AGENT},
|
||||
) as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
response = client.get(
|
||||
uri,
|
||||
follow_redirects=True,
|
||||
headers={"Accept": "application/activity+json"},
|
||||
|
@ -785,7 +772,7 @@ class Identity(StatorModel):
|
|||
response.content,
|
||||
)
|
||||
|
||||
async def fetch_actor(self) -> bool:
|
||||
def fetch_actor(self) -> bool:
|
||||
"""
|
||||
Fetches the user's actor information, as well as their domain from
|
||||
webfinger if it's available.
|
||||
|
@ -796,7 +783,7 @@ class Identity(StatorModel):
|
|||
if self.local:
|
||||
raise ValueError("Cannot fetch local identities")
|
||||
try:
|
||||
response = await SystemActor().signed_request(
|
||||
response = SystemActor().signed_request(
|
||||
method="get",
|
||||
uri=self.actor_uri,
|
||||
)
|
||||
|
@ -810,7 +797,7 @@ class Identity(StatorModel):
|
|||
if status_code >= 400:
|
||||
if status_code == 410 and self.pk:
|
||||
# Their account got deleted, so let's do the same.
|
||||
await Identity.objects.filter(pk=self.pk).adelete()
|
||||
Identity.objects.filter(pk=self.pk).delete()
|
||||
|
||||
if status_code < 500 and status_code not in [401, 403, 404, 406, 410]:
|
||||
capture_message(
|
||||
|
@ -866,44 +853,43 @@ class Identity(StatorModel):
|
|||
)
|
||||
# Now go do webfinger with that info to see if we can get a canonical domain
|
||||
actor_url_parts = urlparse(self.actor_uri)
|
||||
get_domain = sync_to_async(Domain.get_remote_domain)
|
||||
if self.username:
|
||||
webfinger_actor, webfinger_handle = await self.fetch_webfinger(
|
||||
webfinger_actor, webfinger_handle = self.fetch_webfinger(
|
||||
f"{self.username}@{actor_url_parts.hostname}"
|
||||
)
|
||||
if webfinger_handle:
|
||||
webfinger_username, webfinger_domain = webfinger_handle.split("@")
|
||||
self.username = webfinger_username
|
||||
self.domain = await get_domain(webfinger_domain)
|
||||
self.domain = Domain.get_remote_domain(webfinger_domain)
|
||||
else:
|
||||
self.domain = await get_domain(actor_url_parts.hostname)
|
||||
self.domain = Domain.get_remote_domain(actor_url_parts.hostname)
|
||||
else:
|
||||
self.domain = await get_domain(actor_url_parts.hostname)
|
||||
self.domain = Domain.get_remote_domain(actor_url_parts.hostname)
|
||||
# Emojis (we need the domain so we do them here)
|
||||
for tag in get_list(document, "tag"):
|
||||
if tag["type"].lower() in ["toot:emoji", "emoji"]:
|
||||
await sync_to_async(Emoji.by_ap_tag)(self.domain, tag, create=True)
|
||||
Emoji.by_ap_tag(self.domain, tag, create=True)
|
||||
# Mark as fetched
|
||||
self.fetched = timezone.now()
|
||||
try:
|
||||
await sync_to_async(self.save)()
|
||||
self.save()
|
||||
except IntegrityError as e:
|
||||
# See if we can fetch a PK and save there
|
||||
if self.pk is None:
|
||||
try:
|
||||
other_row = await Identity.objects.aget(actor_uri=self.actor_uri)
|
||||
other_row = Identity.objects.get(actor_uri=self.actor_uri)
|
||||
except Identity.DoesNotExist:
|
||||
raise ValueError(
|
||||
f"Could not save Identity at end of actor fetch: {e}"
|
||||
)
|
||||
self.pk: int | None = other_row.pk
|
||||
await sync_to_async(self.save)()
|
||||
self.save()
|
||||
|
||||
# Fetch pinned posts after identity has been fetched and saved
|
||||
if self.featured_collection_uri:
|
||||
featured = await self.fetch_pinned_post_uris(self.featured_collection_uri)
|
||||
featured = self.fetch_pinned_post_uris(self.featured_collection_uri)
|
||||
service = IdentityService(self)
|
||||
await sync_to_async(service.sync_pins)(featured)
|
||||
service.sync_pins(featured)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -1016,7 +1002,7 @@ class Identity(StatorModel):
|
|||
|
||||
### Cryptography ###
|
||||
|
||||
async def signed_request(
|
||||
def signed_request(
|
||||
self,
|
||||
method: Literal["get", "post"],
|
||||
uri: str,
|
||||
|
@ -1025,7 +1011,7 @@ class Identity(StatorModel):
|
|||
"""
|
||||
Performs a signed request on behalf of the System Actor.
|
||||
"""
|
||||
return await HttpSignature.signed_request(
|
||||
return HttpSignature.signed_request(
|
||||
method=method,
|
||||
uri=uri,
|
||||
body=body,
|
||||
|
|
|
@ -2,7 +2,6 @@ from urllib.parse import urlparse
|
|||
|
||||
import httpx
|
||||
import urlman
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db import models
|
||||
|
@ -22,26 +21,25 @@ class ReportStates(StateGraph):
|
|||
new.transitions_to(sent)
|
||||
|
||||
@classmethod
|
||||
async def handle_new(cls, instance: "Report"):
|
||||
def handle_new(cls, instance: "Report"):
|
||||
"""
|
||||
Sends the report to the remote server if we need to
|
||||
"""
|
||||
from users.models import SystemActor, User
|
||||
|
||||
recipients = []
|
||||
report = await instance.afetch_full()
|
||||
async for mod in User.objects.filter(
|
||||
for mod in User.objects.filter(
|
||||
models.Q(moderator=True) | models.Q(admin=True)
|
||||
).values_list("email", flat=True):
|
||||
recipients.append(mod)
|
||||
|
||||
if report.forward and not report.subject_identity.domain.local:
|
||||
if instance.forward and not instance.subject_identity.domain.local:
|
||||
system_actor = SystemActor()
|
||||
try:
|
||||
await system_actor.signed_request(
|
||||
system_actor.signed_request(
|
||||
method="post",
|
||||
uri=report.subject_identity.inbox_uri,
|
||||
body=canonicalise(report.to_ap()),
|
||||
uri=instance.subject_identity.inbox_uri,
|
||||
body=canonicalise(instance.to_ap()),
|
||||
)
|
||||
except httpx.RequestError:
|
||||
pass
|
||||
|
@ -50,7 +48,7 @@ class ReportStates(StateGraph):
|
|||
body=render_to_string(
|
||||
"emails/report_new.txt",
|
||||
{
|
||||
"report": report,
|
||||
"report": instance,
|
||||
"config": Config.system,
|
||||
"settings": settings,
|
||||
},
|
||||
|
@ -62,14 +60,14 @@ class ReportStates(StateGraph):
|
|||
content=render_to_string(
|
||||
"emails/report_new.html",
|
||||
{
|
||||
"report": report,
|
||||
"report": instance,
|
||||
"config": Config.system,
|
||||
"settings": settings,
|
||||
},
|
||||
),
|
||||
mimetype="text/html",
|
||||
)
|
||||
await sync_to_async(email.send)()
|
||||
email.send()
|
||||
return cls.sent
|
||||
|
||||
|
||||
|
@ -145,15 +143,6 @@ class Report(StatorModel):
|
|||
|
||||
### ActivityPub ###
|
||||
|
||||
async def afetch_full(self) -> "Report":
|
||||
return await Report.objects.select_related(
|
||||
"source_identity",
|
||||
"source_domain",
|
||||
"subject_identity__domain",
|
||||
"subject_identity",
|
||||
"subject_post",
|
||||
).aget(pk=self.pk)
|
||||
|
||||
@classmethod
|
||||
def handle_ap(cls, data):
|
||||
"""
|
||||
|
|
|
@ -79,7 +79,7 @@ class SystemActor:
|
|||
],
|
||||
}
|
||||
|
||||
async def signed_request(
|
||||
def signed_request(
|
||||
self,
|
||||
method: Literal["get", "post"],
|
||||
uri: str,
|
||||
|
@ -88,7 +88,7 @@ class SystemActor:
|
|||
"""
|
||||
Performs a signed request on behalf of the System Actor.
|
||||
"""
|
||||
return await HttpSignature.signed_request(
|
||||
return HttpSignature.signed_request(
|
||||
method=method,
|
||||
uri=uri,
|
||||
body=body,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -140,7 +139,7 @@ class Inbox(View):
|
|||
|
||||
if not identity.public_key:
|
||||
# See if we can fetch it right now
|
||||
async_to_sync(identity.fetch_actor)()
|
||||
identity.fetch_actor()
|
||||
|
||||
if not identity.public_key:
|
||||
exceptions.capture_message(
|
||||
|
|
Loading…
Reference in New Issue