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
111 lines
11 KiB
Bash
111 lines
11 KiB
Bash
# Mobilizon 5.2.4: a group and its events. alice_mobilizon follows the group, which announces the events its members
|
|
# organise; an event arrives as an Event with its dates and place; she joins it and leaves; comments both ways; an edit
|
|
# and comments closed; deletes; a group post; an event kept for decePubClient's e2e; the unfollow. An event made through its API without options has its comments closed
|
|
# (`commentsEnabled: false`), which PrivaPub honours, so this one opens them. Mobilizon's API is GraphQL (/api), signed in by its login mutation; what it holds is read from
|
|
# its PostGIS (database mobilizon).
|
|
MZ=https://mobilizon.test:6443
|
|
mz_sql() { podman exec pasture-postgis psql -U pasture -d mobilizon -tAc "$1" 2>/dev/null; }
|
|
# mz <query> [variables as JSON]: a GraphQL request as mzuser, its JSON answer
|
|
mz() {
|
|
curl -sk --resolve mobilizon.test:6443:127.0.0.1 "$MZ/api" -H "Authorization: Bearer $MZT" -H 'Content-Type: application/json' \
|
|
-d "$(python3 -c 'import json, sys; print(json.dumps({"query": sys.argv[1], "variables": json.loads(sys.argv[2])}))' "$1" "${2:-{\}}")"
|
|
}
|
|
p_home_has() { curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next((o['id'] for o in ((s.get('reblog') or s) for s in d) if '$1' in (o['content'] or '') or '$1' in ((o.get('privapub') or {}).get('title') or '')), ''))"; }
|
|
|
|
echo "mobilizon"
|
|
[ -s "$here/.state/mobilizon.token" ] && ok "Mobilizon credentials for mzuser" || { ko "Mobilizon credentials"; return 1; }
|
|
credentials=$(cat "$here/.state/mobilizon.token")
|
|
MZT=$(curl -sk --resolve mobilizon.test:6443:127.0.0.1 "$MZ/api" -H 'Content-Type: application/json' \
|
|
-d "{\"query\":\"mutation{login(email:\\\"${credentials%%:*}\\\",password:\\\"${credentials#*:}\\\"){accessToken}}\"}" | j "print(d['data']['login']['accessToken'])")
|
|
[ -n "$MZT" ] && ok "mzuser signs in to Mobilizon's GraphQL API" || { ko "mzuser cannot sign in"; return 1; }
|
|
PT=$(privapub_token alice_mobilizon)
|
|
PH="Authorization: Bearer $PT"
|
|
[ -n "$PT" ] && ok "PrivaPub token for alice_mobilizon" || { ko "PrivaPub token for alice_mobilizon"; return 1; }
|
|
|
|
echo " the group and its follower"
|
|
me=$(mz '{loggedPerson{id}}' | j "print(d['data']['loggedPerson']['id'])")
|
|
mz 'mutation($n:String!){createGroup(preferredUsername:$n,name:"Pasture events",summary:"<p>events in the pasture</p>",visibility:PUBLIC,openness:OPEN){id}}' \
|
|
'{"n":"pastureevents"}' >/dev/null
|
|
group=$(mz '{group(preferredUsername:"pastureevents"){id}}' | j "print(d['data']['group']['id'])")
|
|
[ -n "$group" ] && ok "mzuser runs the group pastureevents" || ko "the group could not be made"
|
|
group_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=pastureevents@mobilizon.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
[ -n "$group_on_p" ] && ok "PrivaPub resolves @pastureevents@mobilizon.test" || ko "PrivaPub cannot resolve the group"
|
|
# a run before this one left the follow: unfollow first, so the follow below is a new request
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$group_on_p/unfollow"; sleep 3
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$group_on_p/follow"
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$group_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \
|
|
&& ok "alice_mobilizon follows the group (Accept arrived)" || ko "the group's Accept never arrived"
|
|
|
|
echo " events"
|
|
title="A picnic in the pasture $(date +%s)"
|
|
begins=$(date -u -d '+3 days' +%Y-%m-%dT18:00:00Z); ends=$(date -u -d '+3 days' +%Y-%m-%dT21:00:00Z)
|
|
event=$(mz 'mutation($t:String!,$b:DateTime!,$e:DateTime!,$o:ID!,$g:ID){createEvent(title:$t,description:"<p>bring bread</p>",beginsOn:$b,endsOn:$e,organizerActorId:$o,attributedToId:$g,visibility:PUBLIC,joinOptions:FREE,options:{commentModeration:ALLOW_ALL},physicalAddress:{description:"Villa Borghese",locality:"Roma",country:"Italia",geom:"12.4922;41.9142"}){id uuid url}}' \
|
|
"{\"t\":\"$title\",\"b\":\"$begins\",\"e\":\"$ends\",\"o\":\"$me\",\"g\":\"$group\"}")
|
|
event_id=$(echo "$event" | j "print(d['data']['createEvent']['id'])"); event_url=$(echo "$event" | j "print(d['data']['createEvent']['url'])")
|
|
[ -n "$event_id" ] && ok "mzuser organises an event for the group" || ko "the event could not be made: $(echo "$event" | cut -c1-200)"
|
|
until_true 60 '[ -n "$(p_home_has "$title")" ]' && ok "the group's event reaches alice_mobilizon's home" || ko "the event never arrived"
|
|
e_on_p=$(p_home_has "$title")
|
|
event_json=$(curl -s -H "$PH" "$P/api/v1/statuses/$e_on_p")
|
|
[ "$(echo "$event_json" | j "e = (d.get('privapub') or {}).get('event') or {}; print(e.get('start', '')[:16], e.get('end', '')[:16])")" = "${begins:0:16} ${ends:0:16}" ] \
|
|
&& ok "it arrives as an event with its start and end" || ko "the event's dates are missing ($(echo "$event_json" | j "print((d.get('privapub') or {}).get('event'))"))"
|
|
echo "$event_json" | j "e = (d.get('privapub') or {}).get('event') or {}; print(any('Borghese' in ((p.get('name') or '') + (p.get('address') or '')) and p.get('latitude') for p in e.get('places', [])))" | grep -q True \
|
|
&& ok "and with its place, located" || ko "the event's place is missing"
|
|
|
|
echo " participation"
|
|
# alice joins the event (owner decision 2026-10-05): a Join Mobilizon takes at once for a free event, then a Leave
|
|
participation() { curl -s -H "$PH" "$P/api/v1/statuses/$e_on_p" | j "print(((d.get('privapub') or {}).get('event') or {}).get('participation'))"; }
|
|
mz_participant() { mz_sql "select p.role from participants p join actors a on a.id = p.actor_id join events e on e.id = p.event_id where a.url like '%alice_mobilizon%' and e.id = $event_id"; }
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/privapub/v1/statuses/$e_on_p/join"
|
|
until_true 45 '[ "$(mz_participant)" = "participant" ]' && ok "alice_mobilizon's Join makes her a participant on Mobilizon" || ko "the Join never made a participant ($(mz_participant))"
|
|
until_true 45 '[ "$(participation)" = "accepted" ]' && ok "Mobilizon's Accept reaches PrivaPub" || ko "the participation stays $(participation)"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/privapub/v1/statuses/$e_on_p/leave"
|
|
until_true 45 '[ -z "$(mz_participant)" ]' && ok "her Leave takes her off the event on Mobilizon" || ko "she is still a participant after leaving"
|
|
[ "$(participation)" = "None" ] && ok "and PrivaPub forgets it" || ko "PrivaPub still shows a participation"
|
|
|
|
echo " comments"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses" -d "status=a PrivaPub comment on the picnic&in_reply_to_id=$e_on_p&visibility=public"
|
|
until_true 45 '[ "$(mz_sql "select count(*) from comments where text like '"'%a PrivaPub comment on the picnic%'"' and deleted_at is null")" -ge 1 ]' \
|
|
&& ok "alice_mobilizon's reply becomes a comment on the event" || ko "the comment never reached Mobilizon"
|
|
comment=$(mz 'mutation($e:ID!){createComment(eventId:$e,text:"<p>a Mobilizon comment on the picnic</p>"){id}}' "{\"e\":\"$event_id\"}" | j "print(d['data']['createComment']['id'])")
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$e_on_p/context" | j "print(any(\"a Mobilizon comment on the picnic\" in s[\"content\"] for s in d[\"descendants\"]))")" = "True" ]' \
|
|
&& ok "mzuser's comment threads under the event on PrivaPub" || ko "Mobilizon's comment missing on PrivaPub"
|
|
|
|
echo " edits and deletes"
|
|
mz 'mutation($e:ID!,$t:String!){updateEvent(eventId:$e,title:$t){id}}' "{\"e\":\"$event_id\",\"t\":\"$title, moved indoors\"}" >/dev/null
|
|
until_true 45 'curl -s -H "$PH" "$P/api/v1/statuses/$e_on_p" | grep -q "moved indoors"' && ok "the event's edit reaches PrivaPub" || ko "the event's edit not applied"
|
|
mz 'mutation($e:ID!){updateEvent(eventId:$e,options:{commentModeration:CLOSED}){id}}' "{\"e\":\"$event_id\"}" >/dev/null
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$e_on_p" | j "print((d.get(\"privapub\") or {}).get(\"locked\"))")" = "True" ]' \
|
|
&& ok "closing its comments locks the event on PrivaPub" || ko "the event's closed comments not applied"
|
|
[ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$PH" "$P/api/v1/statuses" -d "status=too late&in_reply_to_id=$e_on_p&visibility=public")" = "422" ] \
|
|
&& ok "and a reply to it is refused" || ko "a reply to the closed event was taken"
|
|
mz 'mutation($c:ID!){deleteComment(commentId:$c){id}}' "{\"c\":\"$comment\"}" >/dev/null
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$e_on_p/context" | j "print(any(\"a Mobilizon comment on the picnic\" in s[\"content\"] for s in d[\"descendants\"]))")" = "False" ]' \
|
|
&& ok "mzuser's deleted comment leaves PrivaPub" || ko "the deleted comment stays on PrivaPub"
|
|
mz 'mutation($e:ID!){deleteEvent(eventId:$e){id}}' "{\"e\":\"$event_id\"}" >/dev/null
|
|
until_true 45 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$e_on_p")" = "404" ]' \
|
|
&& ok "the deleted event leaves PrivaPub" || ko "the deleted event stays on PrivaPub"
|
|
|
|
echo " group posts"
|
|
post_title="News from the pasture $(date +%s)"
|
|
mz 'mutation($g:ID!,$t:String!){createPost(attributedToId:$g,title:$t,body:"<p>the picnic moved indoors</p>",visibility:PUBLIC){id}}' \
|
|
"{\"g\":\"$group\",\"t\":\"$post_title\"}" >/dev/null
|
|
until_true 60 '[ -n "$(p_home_has "$post_title")" ]' && ok "the group's post reaches alice_mobilizon's home, with its title" || ko "the group's post never arrived"
|
|
|
|
echo " an event kept for decePub's tests"
|
|
# decePubClient's e2e joins it from its card (tests/e2e/Actions/EventTests.cs); the one a run before this one kept goes
|
|
kept="$here/.state/mobilizon-kept-event.json"
|
|
[ -s "$kept" ] && mz 'mutation($e:ID!){deleteEvent(eventId:$e){id}}' "{\"e\":\"$(j "print(d['id'])" < "$kept")\"}" >/dev/null
|
|
kept_title="A concert kept for decePub $(date +%s)"
|
|
kept_begins=$(date -u -d '+10 days' +%Y-%m-%dT19:00:00Z); kept_ends=$(date -u -d '+10 days' +%Y-%m-%dT22:00:00Z)
|
|
mz 'mutation($t:String!,$b:DateTime!,$e:DateTime!,$o:ID!,$g:ID){createEvent(title:$t,description:"<p>come and listen</p>",beginsOn:$b,endsOn:$e,organizerActorId:$o,attributedToId:$g,visibility:PUBLIC,joinOptions:FREE,options:{commentModeration:ALLOW_ALL},physicalAddress:{description:"Piazza del Popolo",locality:"Roma",country:"Italia",geom:"12.4763;41.9109"}){id url}}' \
|
|
"{\"t\":\"$kept_title\",\"b\":\"$kept_begins\",\"e\":\"$kept_ends\",\"o\":\"$me\",\"g\":\"$group\"}" | j "print(json.dumps(d['data']['createEvent']))" > "$kept"
|
|
until_true 60 '[ -n "$(p_home_has "$kept_title")" ]' && ok "an event kept for decePub's tests reaches PrivaPub" || ko "the kept event never arrived"
|
|
|
|
echo " unfollow"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$group_on_p/unfollow"
|
|
until_true 45 '[ "$(mz_sql "select count(*) from followers f join actors a on a.id = f.actor_id where a.url like '"'%alice_mobilizon%'"'")" = "0" ]' \
|
|
&& ok "alice_mobilizon's unfollow reaches the group" || ko "the group still counts alice_mobilizon"
|
|
|
|
echo " statistics"
|
|
stats_check mobilizon.test mobilizon
|