Skip to content

Commit

Permalink
add relay support
Browse files Browse the repository at this point in the history
  • Loading branch information
alphatownsman committed Sep 4, 2023
1 parent ddf24d3 commit 2dfbeee
Show file tree
Hide file tree
Showing 20 changed files with 377 additions and 20 deletions.
6 changes: 3 additions & 3 deletions activities/models/fan_out.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def handle_new(cls, instance: "FanOut"):
instance.identity.shared_inbox_uri
or instance.identity.inbox_uri
),
body=canonicalise(post.to_create_ap()),
body=canonicalise(post.to_create_ap(), outbound_compat=True),
)
except httpx.RequestError:
return
Expand All @@ -98,7 +98,7 @@ def handle_new(cls, instance: "FanOut"):
instance.identity.shared_inbox_uri
or instance.identity.inbox_uri
),
body=canonicalise(post.to_update_ap()),
body=canonicalise(post.to_update_ap(), outbound_compat=True),
)
except httpx.RequestError:
return
Expand All @@ -124,7 +124,7 @@ def handle_new(cls, instance: "FanOut"):
instance.identity.shared_inbox_uri
or instance.identity.inbox_uri
),
body=canonicalise(post.to_delete_ap()),
body=canonicalise(post.to_delete_ap(), outbound_compat=True),
)
except httpx.RequestError:
return
Expand Down
7 changes: 7 additions & 0 deletions activities/models/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from users.models.hashtag_follow import HashtagFollow
from users.models.identity import Identity, IdentityStates
from users.models.inbox_message import InboxMessage
from users.models.relay_actor import RelayActor
from users.models.system_actor import SystemActor


Expand Down Expand Up @@ -779,6 +780,12 @@ def get_targets(self) -> Iterable[Identity]:
targets.remove(block.target)
except KeyError:
pass
# send local-created public posts to relays
if self.local and self.visibility in [
Post.Visibilities.public,
Post.Visibilities.unlisted,
]:
targets.update(set(RelayActor.get_relays()))
# Now dedupe the targets based on shared inboxes (we only keep one per
# shared inbox)
deduped_targets = set()
Expand Down
8 changes: 7 additions & 1 deletion activities/models/post_interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from core.snowflake import Snowflake
from stator.models import State, StateField, StateGraph, StatorModel
from users.models.identity import Identity
from users.models.relay_actor import RelayActor


class PostInteractionStates(StateGraph):
Expand Down Expand Up @@ -298,6 +299,11 @@ def to_ap(self) -> dict:
"object": self.post.object_uri,
"to": "as:Public",
}
if self.identity.is_local_relay:
# if boost to relay, set "to" to remote relays instead of Public
value["to"] = list(
RelayActor.get_relays().values_list("actor_uri", flat=True)
)
elif self.type == self.Types.like:
value = {
"type": "Like",
Expand All @@ -315,7 +321,7 @@ def to_ap(self) -> dict:
"inReplyTo": self.post.object_uri,
"attributedTo": self.identity.actor_uri,
}
elif self.type == self.Types.pin:
else:
raise ValueError("Cannot turn into AP")
return value

Expand Down
4 changes: 3 additions & 1 deletion activities/views/posts.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def serve_object(self):
if not self.post_obj.local:
return redirect(self.post_obj.object_uri)
return JsonResponse(
canonicalise(self.post_obj.to_ap(), include_security=True),
canonicalise(
self.post_obj.to_ap(), include_security=True, outbound_compat=True
),
content_type="application/activity+json",
)
22 changes: 17 additions & 5 deletions core/ld.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"attachment": {"@id": "as:attachment", "@type": "@id"},
"bcc": {"@id": "as:bcc", "@type": "@id"},
"bto": {"@id": "as:bto", "@type": "@id"},
"cc": {"@id": "as:cc", "@type": "@id"},
"cc": {"@id": "as:cc", "@type": "@id", "@container": "@set"},
"context": {"@id": "as:context", "@type": "@id"},
"current": {"@id": "as:current", "@type": "@id"},
"first": {"@id": "as:first", "@type": "@id"},
Expand Down Expand Up @@ -119,7 +119,7 @@
"partOf": {"@id": "as:partOf", "@type": "@id"},
"tag": {"@id": "as:tag", "@type": "@id"},
"target": {"@id": "as:target", "@type": "@id"},
"to": {"@id": "as:to", "@type": "@id"},
"to": {"@id": "as:to", "@type": "@id", "@container": "@set"},
"url": {"@id": "as:url", "@type": "@id"},
"altitude": {"@id": "as:altitude", "@type": "xsd:float"},
"content": "as:content",
Expand Down Expand Up @@ -594,7 +594,9 @@ def builtin_document_loader(url: str, options={}):
)


def canonicalise(json_data: dict, include_security: bool = False) -> dict:
def canonicalise(
json_data: dict, include_security: bool = False, outbound_compat: bool = False
) -> dict:
"""
Given an ActivityPub JSON-LD document, round-trips it through the LD
systems to end up in a canonicalised, compacted format.
Expand Down Expand Up @@ -632,8 +634,18 @@ def canonicalise(json_data: dict, include_security: bool = False) -> dict:
context.append("https://w3id.org/security/v1")

json_data["@context"] = context

return jsonld.compact(jsonld.expand(json_data), context)
j = jsonld.compact(jsonld.expand(json_data), context)
if outbound_compat:
# patch outbound json to make it compatible with various implementations
for k in ["to", "cc"]:
if j.get(k):
j[k] = [
x
if x != "as:Public"
else "https://www.w3.org/ns/activitystreams#Public"
for x in j[k]
]
return j


def get_list(container, key) -> list:
Expand Down
2 changes: 1 addition & 1 deletion core/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ class SystemOptions(pydantic.BaseModel):
cache_timeout_page_post: int = 60 * 2
cache_timeout_identity_feed: int = 60 * 5

restricted_usernames: str = "admin\nadmins\nadministrator\nadministrators\nsystem\nroot\nannounce\nannouncement\nannouncements"
restricted_usernames: str = "__system__\n__relay__\nadmin\nadmins\nadministrator\nadministrators\nsystem\nroot\nannounce\nannouncement\nannouncements"

custom_head: str | None

Expand Down
6 changes: 6 additions & 0 deletions takahe/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@
"admin/domains/<domain>/delete/",
admin.DomainDelete.as_view(),
),
path(
"admin/relays/",
admin.RelayRoot.as_view(),
name="admin_relays",
),
path(
"admin/federation/",
admin.FederationRoot.as_view(),
Expand Down Expand Up @@ -342,6 +347,7 @@
path("actor/inbox/", activitypub.Inbox.as_view()),
path("actor/outbox/", activitypub.EmptyOutbox.as_view()),
path("inbox/", activitypub.Inbox.as_view(), name="shared_inbox"),
path("relay", activitypub.RelayActorView.as_view()),
# API/Oauth
path("api/", include("api.urls")),
path("oauth/authorize", oauth.AuthorizationView.as_view()),
Expand Down
4 changes: 4 additions & 0 deletions templates/admin/_menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ <h3>Administration</h3>
<i class="fa-solid fa-diagram-project"></i>
<span>Federation</span>
</a>
<a href="{% url "admin_relays" %}" {% if section == "relays" %}class="selected"{% endif %} title="Relays">
<i class="fa-solid fa-tower-broadcast"></i>
<span>Relays</span>
</a>
<a href="{% url "admin_users" %}" {% if section == "users" %}class="selected"{% endif %} title="Users">
<i class="fa-solid fa-users"></i>
<span>Users</span>
Expand Down
1 change: 1 addition & 0 deletions templates/admin/identities.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
Remote
<small>{{ identity.followers_count }} local follower{{ identity.followers_count|pluralize }}</small>
{% endif %}
{{ identity.actor_type }}
</td>
<td class="actions">
<a href="{{ identity.urls.admin_edit }}" title="View"><i class="fa-solid fa-eye"></i></a>
Expand Down
69 changes: 69 additions & 0 deletions templates/admin/relays.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{% extends "admin/base_main.html" %}
{% load activity_tags %}

{% block subtitle %}Relay{% endblock %}

{% block settings_content %}
<form action="?subscribe" method="post" class="search">
<input type="url" name="actor_uri" value="" placeholder="relay actor URI, e.g. https://relay.server/actor">
{% csrf_token %}
<button>Subscribe</button>
</form>
<table class="items">
{% for identity in page_obj %}
<tr>
<td class="icon">

</td>
<td class="name">
{{ identity.actor_uri }}
<small>{{ identity.domain.nodeinfo.metadata.nodeName }}</small>
{% if identity.domain.nodeinfo.software %}
<small>{{ identity.domain.nodeinfo.software.name }} / {{ identity.domain.nodeinfo.software.version }}</small>
{% endif %}
</td>
<td>
{% if identity.restriction == 1 %}
<span class="bad">Limited</span>
{% elif identity.restriction == 2 %}
<span class="bad">Blocked</span>
{% endif %}
</td>
<td class="stat">
{% if identity.follow_state == 'accepting' or identity.follow_state == 'accepted' %}
ACTIVE
{% elif identity.follow_state == 'unrequested' or identity.follow_state == 'pending_approval' %}
ACTIVATING
{% else %}
DEACTIVATING
{% endif %}
<small>({{ identity.follow_state }})</small>
</td>
<td class="actions">
<form action="?unsubscribe" method="post">
<input type="hidden" name="actor_uri" value="{{ identity.actor_uri }}">
{% csrf_token %}
<button>Unsubscribe</button>
</form>
</td>
<td class="actions">
<form action="?remove" method="post">
<input type="hidden" name="actor_uri" value="{{ identity.actor_uri }}">
{% csrf_token %}
<button onclick="return confirm('Sure to force remove?')" {%if identity.follow_state == 'accepting' or identity.follow_state == 'accepted'%}disabled{%endif%}>Remove</button>
</form>
</td>
</tr>
{% empty %}
<tr class="empty">
<td>
There are no relay yet.
</td>
</tr>
{% endfor %}
</table>
<div class="view-options">
<small><i class="fa-regular fa-lightbulb"></i>&nbsp; invalid actor uri will not show in this list; (un)subscribing may take a while; use remove only when it's stuck in (DE)ACTIVATING state for a long time.</small>
</div>
{% include "admin/_pagination.html" with nouns="relay,relays" %}
{% endblock %}
3 changes: 2 additions & 1 deletion users/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ def data_init(self, **kwargs):
boot (or post upgrade).
"""
# Generate the server actor keypair if needed
from users.models import SystemActor
from users.models import RelayActor, SystemActor

SystemActor.generate_keys_if_needed()
RelayActor.initialize_if_needed()

def ready(self) -> None:
post_migrate.connect(self.data_init, sender=self)
1 change: 1 addition & 0 deletions users/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .inbox_message import InboxMessage, InboxMessageStates # noqa
from .invite import Invite # noqa
from .password_reset import PasswordReset # noqa
from .relay_actor import RelayActor # noqa
from .report import Report # noqa
from .system_actor import SystemActor # noqa
from .user import User # noqa
Expand Down
8 changes: 6 additions & 2 deletions users/models/follow.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def handle_unrequested(cls, instance: "Follow"):
instance.source.signed_request(
method="post",
uri=instance.target.inbox_uri,
body=canonicalise(instance.to_ap()),
body=canonicalise(instance.to_ap(), outbound_compat=True),
)
except httpx.RequestError:
return
Expand Down Expand Up @@ -242,7 +242,11 @@ def create_local(cls, source, target, boosts=True):
uri="",
state=FollowStates.unrequested,
)
follow.uri = source.actor_uri + f"follow/{follow.pk}/"
follow.uri = (
source.actor_uri
+ ("/" if source.actor_uri[-1] != "/" else "")
+ f"follow/{follow.pk}/"
)
follow.save()
return follow

Expand Down
5 changes: 5 additions & 0 deletions users/models/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from stator.models import State, StateField, StateGraph, StatorModel
from users.models.domain import Domain
from users.models.relay_actor import RelayActor
from users.models.system_actor import SystemActor


Expand Down Expand Up @@ -337,6 +338,10 @@ def safe_metadata(self):
for data in self.metadata
]

@property
def is_local_relay(self):
return self.local and self.actor_uri == RelayActor.actor_uri

def ensure_uris(self):
"""
Ensures that local identities have all the URIs populated on their fields
Expand Down
5 changes: 5 additions & 0 deletions users/models/inbox_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from core.exceptions import ActivityPubError
from stator.models import State, StateField, StateGraph, StatorModel
from users.models.relay_actor import RelayActor


class InboxMessageStates(StateGraph):
Expand Down Expand Up @@ -141,6 +142,10 @@ def handle_received(cls, instance: "InboxMessage"):
IdentityService.handle_internal_add_follow(
instance.message["object"]
)
case "unfollowrelay":
RelayActor.handle_internal_unfollow(
instance.message["object"]
)
case unknown:
return cls.errored
case unknown:
Expand Down
Loading

0 comments on commit 2dfbeee

Please sign in to comment.