Pasture: Friendica joins, in the scenarios and the town
Friendica 2026.05 on the shared MySQL and Redis, with its worker daemon as a sidecar. friendica_settle repairs what its install leaves: the system user has no name, so the system account that signs its fetches is never made; the web container cannot see the sidecar's daemon, so a queued job waits for the five-minute cron unless the daemon is declared running; its log needs a file and debugging on. Accounts are saved through its API with locked=0 (a number: "true" reads as 0, unlocked), which makes them soapbox pages that take followers without following back, and each one's outbox is read once: a Follow that reaches an account Friendica has not cached makes it fetch the account from itself, signed, and checking that signature recursed for five minutes, holding PrivaPub's first follow past its timeout. scenarios/friendica.sh passes its 25 checks from a clean install: follows and unfollows, posts (a titled one with its title), comments, likes, Friendica's dislike as a downvote, boosts, edits (through its web editor: its Mastodon API never federates one) and deletes, both ways. The town gets a Friendica driver (HTTP Basic, its MySQL read through mysql_json, edits in the web editor, no bookmark on a reply) and specs/friendica-pair.json, which passes its 275 checks. On the way: - the checker knows Friendica's thread model: a non-public reply reaches an account only under posts it holds, and a Friendica account's non-public reply in a thread another server owns reaches nobody else there; - the seeder answers a follow request the target still holds whatever the follower's server says: Friendica reports a follow of someone already following its account as made at once (and shows that persona's followers-only posts while a locked persona still holds the request); - the selftest skips polls where a platform has none; the shared MySQL helpers move to peers/shared.sh. 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
e485f7bd47
commit
a5d493a8c9
17 files changed
+513
-31
No files matched your search
@@ -0,0 +1,103 @@
|
||||
# Friendica 2026.05: follows both ways; posts both ways, a titled one as an Article; comments both ways; likes both
|
||||
# ways, Friendica's dislike as a downvote, a boost; edits, deletes and unfollows both ways; statistics. Friendica keeps every
|
||||
# activity (a like, a dislike, a boost) as an item of its own with a verb, so what it holds is read from its MySQL
|
||||
# (database friendica) by `thr-parent`, never fetched. Its API takes HTTP Basic authentication. An edit made through its
|
||||
# Mastodon API never federates (Statuses::put leaves `edited` alone, and only a changed `edited` notifies), so fruser
|
||||
# edits through the web editor, signed in with a cookie.
|
||||
FR=https://friendica.test:6443
|
||||
frc() { curl -sk --resolve friendica.test:6443:127.0.0.1 -u "$(cat "$here/.state/friendica.token" 2>/dev/null)" -H 'Accept: application/json' "$@"; }
|
||||
fr_sql() { podman exec pasture-mysql mysql -upasture -ppasture friendica -Nse "$1" 2>/dev/null; }
|
||||
# fr_count <uri> <verb>: the live activities of a verb on a post Friendica holds, by the verb's last part ("/like",
|
||||
# "/dislike", "#Announce": a boost that arrives keeps ActivityStreams' name, where Friendica's own are "/share")
|
||||
fr_count() { fr_sql "select count(*) from \`post-view\` where \`thr-parent\` = '$1' and verb like '%$2' and deleted = 0"; }
|
||||
# fr_web_edit <status id> <text>: fruser edits a post through the web editor (the API's status id is the uri-id)
|
||||
fr_web_edit() {
|
||||
local jar="$here/.state/friendica.cookies" credentials item
|
||||
credentials=$(cat "$here/.state/friendica.token")
|
||||
curl -sk --resolve friendica.test:6443:127.0.0.1 -c "$jar" -o /dev/null -X POST "$FR/login" -d auth-params=login -d username=fruser \
|
||||
--data-urlencode "password=${credentials#*:}"
|
||||
item=$(fr_sql "select id from \`post-user\` where \`uri-id\` = $1 and uid = (select uid from user where nickname = 'fruser')")
|
||||
curl -sk --resolve friendica.test:6443:127.0.0.1 -b "$jar" -o /dev/null -X POST "$FR/item" -d "post_id=$item" --data-urlencode "body=$2"
|
||||
}
|
||||
p_home_id() { 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 "friendica"
|
||||
[ -s "$here/.state/friendica.token" ] && ok "Friendica credentials for fruser" || { ko "Friendica credentials"; return 1; }
|
||||
PT=$(privapub_token alice_friendica)
|
||||
PH="Authorization: Bearer $PT"
|
||||
[ -n "$PT" ] && ok "PrivaPub token for alice_friendica" || { ko "PrivaPub token for alice_friendica"; return 1; }
|
||||
|
||||
echo " discovery and follows"
|
||||
alice_on_fr=$(frc "$FR/api/v2/search?q=@alice_friendica@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
||||
[ -n "$alice_on_fr" ] && ok "Friendica resolves @alice_friendica@privapub.test" || ko "Friendica cannot resolve alice_friendica"
|
||||
fr_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=fruser@friendica.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
||||
[ -n "$fr_on_p" ] && ok "PrivaPub resolves @fruser@friendica.test" || ko "PrivaPub cannot resolve fruser"
|
||||
# a run cut short, or a Friendica made anew, leaves follows behind on one side only: unfollow first, so both follows
|
||||
# below are new requests
|
||||
frc -o /dev/null -X POST "$FR/api/v1/accounts/$alice_on_fr/unfollow"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$fr_on_p/unfollow"
|
||||
sleep 3
|
||||
frc -o /dev/null -X POST "$FR/api/v1/accounts/$alice_on_fr/follow"
|
||||
until_true 45 '[ "$(frc "$FR/api/v1/accounts/relationships?id[]=$alice_on_fr" | j "print(d[0][\"following\"])")" = "True" ]' \
|
||||
&& ok "fruser follows alice_friendica (Accept arrived)" || ko "Friendica's follow not accepted"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$fr_on_p/follow"
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$fr_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \
|
||||
&& ok "alice_friendica follows fruser (Accept arrived)" || ko "PrivaPub's follow not accepted"
|
||||
|
||||
echo " posts"
|
||||
p_post=$(curl -s -X POST -H "$PH" "$P/api/v1/statuses" -d 'status=Hello Friendica from PrivaPub&visibility=public')
|
||||
p_post_id=$(echo "$p_post" | j "print(d['id'])"); p_post_uri=$(echo "$p_post" | j "print(d['uri'])")
|
||||
until_true 45 '[ "$(fr_sql "select count(*) from \`post-view\` where uri = '"'$p_post_uri'"' and deleted = 0")" -ge 1 ]' \
|
||||
&& ok "alice_friendica's post reaches Friendica" || ko "alice_friendica's post missing on Friendica"
|
||||
fr_post=$(frc -X POST "$FR/api/v1/statuses" -d 'status=Hello PrivaPub from Friendica&visibility=public' | j "print(d['id'])")
|
||||
until_true 45 '[ -n "$(p_home_id "Hello PrivaPub from Friendica")" ]' && ok "fruser's post reaches alice_friendica's home" || ko "fruser's post never arrived"
|
||||
fr_on_p_post=$(p_home_id "Hello PrivaPub from Friendica")
|
||||
fr_post_uri=$(curl -s -H "$PH" "$P/api/v1/statuses/$fr_on_p_post" | j "print(d['uri'])")
|
||||
frc -o /dev/null -X POST "$FR/api/v1/statuses" -d 'status=An article with paragraphs.&friendica[title]=A Friendica article&visibility=public'
|
||||
until_true 45 '[ -n "$(p_home_id "A Friendica article")" ]' && ok "fruser's titled post arrives with its title" || ko "the titled post never arrived, or lost its title"
|
||||
|
||||
echo " comments"
|
||||
frc -o /dev/null -X POST "$FR/api/v1/statuses" -d "status=@alice_friendica@privapub.test a Friendica comment&in_reply_to_id=$(frc "$FR/api/v2/search?q=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$p_post_uri")&resolve=true&type=statuses" | j "print(d['statuses'][0]['id'])")&visibility=public"
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id/context" | j "print(any(\"a Friendica comment\" in s[\"content\"] for s in d[\"descendants\"]))")" = "True" ]' \
|
||||
&& ok "fruser's comment threads under alice_friendica's post" || ko "Friendica's comment missing on PrivaPub"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses" -d "status=@fruser@friendica.test a PrivaPub comment&in_reply_to_id=$fr_on_p_post&visibility=public"
|
||||
until_true 45 '[ "$(fr_sql "select count(*) from \`post-view\` where \`thr-parent\` = '"'$fr_post_uri'"' and body like '"'%a PrivaPub comment%'"'")" -ge 1 ]' \
|
||||
&& ok "alice_friendica's comment threads under fruser's post on Friendica" || ko "alice_friendica's comment missing on Friendica"
|
||||
|
||||
echo " likes, dislikes and boosts"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$fr_on_p_post/favourite"
|
||||
until_true 45 '[ "$(fr_count "$fr_post_uri" /like)" = "1" ]' && ok "alice_friendica's like counts on Friendica" || ko "like not counted on Friendica"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$fr_on_p_post/reblog"
|
||||
until_true 45 '[ "$(fr_count "$fr_post_uri" "#Announce")" = "1" ]' && ok "alice_friendica's boost counts on Friendica" || ko "boost not counted on Friendica"
|
||||
p_on_fr=$(frc "$FR/api/v2/search?q=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$p_post_uri")&resolve=true&type=statuses" | j "print(d['statuses'][0]['id'])")
|
||||
frc -o /dev/null -X POST "$FR/api/v1/statuses/$p_on_fr/favourite"
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(d[\"favourites_count\"])")" = "1" ]' && ok "fruser's like counts on PrivaPub" || ko "Friendica's like not counted"
|
||||
frc -o /dev/null -X POST "$FR/api/v1/statuses/$p_on_fr/unfavourite"
|
||||
frc -o /dev/null -X POST "$FR/api/friendica/activity/dislike" -d "id=$p_on_fr"
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(d[\"favourites_count\"], ((d.get(\"privapub\") or {}).get(\"votes\") or {}).get(\"down\"))")" = "0 1" ]' \
|
||||
&& ok "fruser's dislike counts as a downvote on PrivaPub" || ko "Friendica's dislike not counted"
|
||||
|
||||
echo " edits and deletes"
|
||||
curl -s -o /dev/null -X PUT -H "$PH" "$P/api/v1/statuses/$p_post_id" -d 'status=Hello Friendica from PrivaPub, edited'
|
||||
until_true 45 '[ "$(fr_sql "select count(*) from \`post-view\` where uri = '"'$p_post_uri'"' and body like '"'%edited%'"'")" = "1" ]' \
|
||||
&& ok "alice_friendica's edit reaches Friendica" || ko "PrivaPub's edit not applied on Friendica"
|
||||
fr_web_edit "$fr_post" 'Hello PrivaPub from Friendica, edited'
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$fr_on_p_post" | j "print(\"edited\" in d[\"content\"])")" = "True" ]' \
|
||||
&& ok "fruser's edit reaches PrivaPub" || ko "Friendica's edit not applied"
|
||||
curl -s -o /dev/null -X DELETE -H "$PH" "$P/api/v1/statuses/$p_post_id"
|
||||
until_true 45 '[ "$(fr_sql "select count(*) from \`post-view\` where uri = '"'$p_post_uri'"' and deleted = 0")" = "0" ]' \
|
||||
&& ok "alice_friendica's delete reaches Friendica" || ko "delete not applied on Friendica"
|
||||
frc -o /dev/null -X DELETE "$FR/api/v1/statuses/$fr_post"
|
||||
until_true 45 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$fr_on_p_post")" = "404" ]' \
|
||||
&& ok "fruser's delete reaches PrivaPub" || ko "Friendica's delete not applied"
|
||||
|
||||
echo " unfollows"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$fr_on_p/unfollow"
|
||||
until_true 45 '[ "$(frc "$FR/api/v1/accounts/relationships?id[]=$alice_on_fr" | j "print(d[0][\"followed_by\"])")" = "False" ]' \
|
||||
&& ok "alice_friendica's unfollow reaches Friendica" || ko "PrivaPub's unfollow not applied on Friendica"
|
||||
frc -o /dev/null -X POST "$FR/api/v1/accounts/$alice_on_fr/unfollow"
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$fr_on_p" | j "print(d[0][\"followed_by\"])")" = "False" ]' \
|
||||
&& ok "fruser's unfollow reaches PrivaPub" || ko "Friendica's unfollow not applied"
|
||||
|
||||
echo " statistics"
|
||||
stats_check friendica.test friendica
|
||||
Reference in new issue
Block a user