# 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 : 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 : 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