Replies to a persona's posts reach its followers; personas join remote events
Two owner decisions of 2026-10-05, recorded in the roadmap.
Replies passed on ("the fediverse is broken without"): a public or unlisted
reply from another server to a persona's public, unlisted or followers-only
post goes on to the persona's followers as its author's server sent it, as
Mastodon forwards it, never to the replier's own server, never for a
local-only or group post; its edit and deletion follow. Only an activity its
own actor delivered is passed on (Arrival.Raw), so nothing forwarded is
forwarded again. The town checks it as relay.reply cells (specs/relay-five:
882 checks pass); Mastodon takes a passed-on activity only with an LD
signature, which GoToSocial and Akkoma don't add, and the checker knows it.
Events: a persona joins another server's event with a Join and leaves it with
a Leave, both to the organiser only, through
POST /api/privapub/v1/statuses/:id/join|leave; the organiser's Accept or
Reject is routed by our join id and shows as privapub.event.participation.
Events by invitation or taken on another site are refused before anything
is sent. Mobilizon's scenario joins and leaves an event (28 checks) and keeps
one for decePubClient's e2e.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
2b42377b3f
commit
0646de22bd
24 files changed
+676
-26
No files matched your search
@@ -3,7 +3,7 @@ one does hold (its database) and show (its API, as one account). Writes out/<run
|
||||
|
||||
Each check is a cell `feature|origin|observer|direction`:
|
||||
in a peer's object as PrivaPub holds it out PrivaPub's object as a peer holds it
|
||||
via a peer's object on another peer, through PrivaPub (a community it hosts)
|
||||
via a peer's object on another peer, through PrivaPub (a community it hosts, or a reply to a persona it relays)
|
||||
control a peer's object on another peer, PrivaPub not involved (tells a PrivaPub bug from a peer quirk)
|
||||
local an object on its own server
|
||||
Known gaps (gaps.json) turn a failure into xfail, and a pass of a known gap into XPASS."""
|
||||
@@ -36,6 +36,11 @@ NEEDS_PICTURE = {"pixelfed"}
|
||||
# posts above it, and a Friendica account's non-public reply in a thread another server owns reaches none of the
|
||||
# others there (nobody relays a followers-only reply), nor anything under it
|
||||
THREAD_BOUND = {"friendica"}
|
||||
# Mastodon takes an activity another server passes on only with its author's LD signature (or a FEP-8b32 proof) and
|
||||
# drops it otherwise (ActivityPub::ProcessActivityService); of the platforms here only Mastodon and Misskey and its forks
|
||||
# sign their Creates that way, so another's reply PrivaPub passes on never lands there
|
||||
NEEDS_LD_SIGNATURE = {"mastodon"}
|
||||
LD_SIGNS = {"mastodon", "misskey", "sharkey"}
|
||||
|
||||
|
||||
class World:
|
||||
@@ -179,10 +184,11 @@ class Sweep:
|
||||
stored = self.stored_all()
|
||||
self.checks = []
|
||||
self.delivery(stored)
|
||||
self.relayed(stored)
|
||||
self.counts(stored)
|
||||
self.threads(stored)
|
||||
self.lifecycle(stored)
|
||||
failing = [c for c in self.checks if not c["ok"] and c["feature"].startswith(("deliver", "count", "edit", "delete", "thread"))]
|
||||
failing = [c for c in self.checks if not c["ok"] and c["feature"].startswith(("deliver", "relay", "count", "edit", "delete", "thread"))]
|
||||
if not failing or time.time() - started > deadline:
|
||||
break
|
||||
print(f" {len(failing)} checks still failing; sweeping again in 20s", flush=True)
|
||||
@@ -225,6 +231,28 @@ class Sweep:
|
||||
elif kind in ("followers", "direct", "circle", "located") and p not in want:
|
||||
self.add(f"confine.{kind}", o["origin"], p, not held, ref, "absent", "held" if held else "absent", through)
|
||||
|
||||
# -- a peer's public or unlisted reply to a persona's post, which PrivaPub passes on to the persona's followers (owner
|
||||
# decision 2026-10-05): held on every server where a follower of the persona lives and that the reply was not
|
||||
# addressed to already
|
||||
def relayed(self, stored):
|
||||
for ref, o in self.w.objects.items():
|
||||
parent = self.w.objects.get(o.get("parent") or "")
|
||||
if ref in self.w.deleted or parent is None or parent["ref"] in self.w.deleted or o["origin"] == "privapub" \
|
||||
or parent["origin"] != "privapub" or parent.get("group") or o["visibility"] not in ("public", "unlisted") \
|
||||
or parent["visibility"] not in ("public", "unlisted", "followers"):
|
||||
continue
|
||||
addressed = {self.w.platform(k) for k in self.w.recipients(o)} | {o["origin"], "privapub"}
|
||||
for p in sorted({self.w.platform(k) for k in self.w.follows.get(parent["author"], ())} - addressed):
|
||||
# (a reply the seeder had p fetch proves nothing about passing it on)
|
||||
if p not in self.platforms or RECEIVES.get(p) is not None or p in o.get("fetched_on", ()) \
|
||||
or (p in NEEDS_LD_SIGNATURE and o["origin"] not in LD_SIGNS):
|
||||
continue
|
||||
if p in NEEDS_PICTURE and not (stored.get(p, {}).get(parent["uri"]) and stored[p][parent["uri"]].exists):
|
||||
continue
|
||||
row = stored.get(p, {}).get(o["uri"])
|
||||
held = bool(row and row.exists and not row.deleted)
|
||||
self.add("relay.reply", o["origin"], p, held, ref, "held", "held" if held else "missing", True)
|
||||
|
||||
# -- counts on the object's own server, and PrivaPub's exact counts
|
||||
def counts(self, stored):
|
||||
likes, reacts, boosts, votes = defaultdict(int), defaultdict(lambda: defaultdict(int)), defaultdict(int), defaultdict(lambda: defaultdict(int))
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"schema": "pasture-town/1",
|
||||
"name": "relay-five",
|
||||
"seed": 20261051,
|
||||
"peers": {
|
||||
"privapub": {
|
||||
"roots": 1,
|
||||
"personas": [
|
||||
4,
|
||||
4
|
||||
],
|
||||
"circles": 0,
|
||||
"communities": 0
|
||||
},
|
||||
"gts": {
|
||||
"accounts": 2
|
||||
},
|
||||
"mastodon": {
|
||||
"accounts": 2
|
||||
},
|
||||
"akkoma": {
|
||||
"accounts": 2
|
||||
},
|
||||
"misskey": {
|
||||
"accounts": 2
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"locked": 0.2,
|
||||
"bot": 0.0,
|
||||
"fields": [
|
||||
0,
|
||||
2
|
||||
],
|
||||
"avatar": 0.9,
|
||||
"header": 0.3,
|
||||
"bioWords": [
|
||||
5,
|
||||
12
|
||||
],
|
||||
"langs": {
|
||||
"en": 80,
|
||||
"it": 20
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"follows": [
|
||||
3,
|
||||
5
|
||||
],
|
||||
"mutual": 0.5,
|
||||
"pending": 0.0,
|
||||
"rejected": 0.0
|
||||
},
|
||||
"circleMembers": 0,
|
||||
"content": {
|
||||
"posts": 36,
|
||||
"mix": {
|
||||
"text": 60,
|
||||
"cw": 10,
|
||||
"tags": 10,
|
||||
"mention": 10,
|
||||
"image": 10
|
||||
},
|
||||
"visibility": {
|
||||
"public": 70,
|
||||
"unlisted": 15,
|
||||
"followers": 15
|
||||
},
|
||||
"replyRounds": 2,
|
||||
"replies": 0.6,
|
||||
"deeperReplies": 0.4
|
||||
},
|
||||
"interactions": {
|
||||
"likes": [
|
||||
0,
|
||||
2
|
||||
],
|
||||
"boosts": [
|
||||
0,
|
||||
1
|
||||
],
|
||||
"react": 0.2,
|
||||
"vote": 0.0,
|
||||
"bookmark": 0.0
|
||||
},
|
||||
"mutations": {
|
||||
"edit": 0.1,
|
||||
"delete": 0.05,
|
||||
"blocks": 0,
|
||||
"mutes": 0,
|
||||
"reports": 0
|
||||
},
|
||||
"time": {
|
||||
"roundSettleSeconds": 15,
|
||||
"graphSettleSeconds": 20,
|
||||
"settleSeconds": 30,
|
||||
"deadlineSeconds": 120
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user