# Ktistec 3.13.0: follows both ways; posts both ways; replies both ways; likes and boosts both ways with their undos; a # poll from Ktistec and alice's vote counted there; FEP-044f quotes both ways; edits and deletions both ways; the # unfollow; statistics. Ktistec speaks part of the Mastodon API (posts, replies, likes, boosts, timelines, # relationships) with the bearer token of a signed-in session; what it has no route for there (follows, polls, edits, # deletions, quotes) goes through its own outbox API, and what it holds (likes, boosts and votes included: its API counts # none) is read from a copy of its SQLite database. . "$here/peers/ktistec.sh" K=https://ktistec.test:6443 KT=$(cat "$here/.state/ktistec/ktuser.token" 2>/dev/null) kc() { site ktistec.test -s -H "Authorization: Bearer $KT" "$@"; } # kn : an activity put in ktuser's outbox (Ktistec's own API), answering its status code kn() { kc -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' "$K/actors/ktuser/outbox" -d "$1"; } # kt_sql : Ktistec's database, read from a copy kt_sql() { local d d=$(mktemp -d) podman cp pasture-ktistec:/db/ktistec.db "$d/" 2>/dev/null python3 -c "import sqlite3, sys; print('\n'.join('|'.join(map(str, r)) for r in sqlite3.connect(sys.argv[1]).execute(sys.argv[2])))" "$d/ktistec.db" "$1" rm -rf "$d" } k_home_id() { kc "$K/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 '')), ''))"; } k_status() { kc "$K/api/v1/statuses/$1" | j "print(d.get('$2'))"; } # k_by_alice : alice's likes and boosts of it that Ktistec holds (its API's counts stay at 0) k_by_alice() { kt_sql "select count(*) from activities where type in ('ActivityPub::Activity::Like', 'ActivityPub::Activity::Announce') and actor_iri = '$alice_uri' and object_iri = '$1' and undone_at is null"; } 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 '')), ''))"; } p_status() { curl -s -H "$PH" "$P/api/v1/statuses/$1" | j "print(d.get('$2'))"; } p_replies_have() { [ "$(curl -s -H "$PH" "$P/api/v1/statuses/$1/context" | j "print(any('$2' in s['content'] for s in d['descendants']))")" = "True" ]; } echo "ktistec" [ -n "$(kc "$K/api/v1/accounts/verify_credentials" | j "print(d.get('username') or '')")" ] && ok "Ktistec's API as ktuser" \ || { ko "Ktistec's API ($(kc "$K/api/v1/accounts/verify_credentials" | head -c 200))"; return 1; } PT=$(privapub_token alice_ktistec) PH="Authorization: Bearer $PT" [ -n "$PT" ] && ok "PrivaPub token for alice_ktistec" || { ko "PrivaPub token for alice_ktistec"; return 1; } alice_uri="$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d['url'])" | sed 's|/@|/peasants/|')" kt_uri=https://ktistec.test/actors/ktuser run=$(date +%s) echo " follows" kt_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=ktuser@ktistec.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") [ -n "$kt_on_p" ] && ok "PrivaPub resolves ktuser" || ko "PrivaPub cannot resolve ktuser" alice_on_k=$(kc "$K/api/v1/accounts/lookup?acct=alice_ktistec@privapub.test" | j "print(d['id'])") [ -n "$alice_on_k" ] && ok "Ktistec resolves alice" || ko "Ktistec cannot resolve alice" # (a run before leaves both following: both follows below are made anew) curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$kt_on_p/unfollow" kc -o /dev/null -X POST "$K/api/v1/accounts/$alice_on_k/unfollow" until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$kt_on_p" | j "print(d[0][\"followed_by\"])")" = "False" ]' >/dev/null kn "{\"type\":\"Follow\",\"object\":\"$alice_uri\"}" >/dev/null until_true 60 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$kt_on_p" | j "print(d[0][\"followed_by\"])")" = "True" ]' \ && ok "ktuser follows alice" || ko "Ktistec's follow never reached alice" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$kt_on_p/follow" until_true 60 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$kt_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \ && ok "alice follows ktuser (Accept arrived)" || ko "Ktistec's Accept never arrived" echo " posts" k_post=$(kc -X POST "$K/api/v1/statuses" -d "status=a Ktistec post $run&visibility=public" | j "print(d['id'])") until_true 60 '[ -n "$(p_home_id "a Ktistec post $run")" ]' && ok "ktuser's post reaches alice's home" || ko "ktuser's post never reached alice" k_post_on_p=$(p_home_id "a Ktistec post $run") k_post_uri=$(p_status "$k_post_on_p" uri) p_post=$(curl -s -X POST -H "$PH" "$P/api/v1/statuses" -d "status=a PrivaPub post for Ktistec $run&visibility=public") p_post_id=$(echo "$p_post" | j "print(d['id'])"); p_post_uri=$(echo "$p_post" | j "print(d['uri'])") until_true 60 '[ -n "$(k_home_id "a PrivaPub post for Ktistec $run")" ]' && ok "alice's post reaches ktuser's home" || ko "alice's post never reached Ktistec" p_post_on_k=$(k_home_id "a PrivaPub post for Ktistec $run") echo " replies" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses" -d "status=@ktuser@ktistec.test a PrivaPub reply $run&in_reply_to_id=$k_post_on_p&visibility=public" until_true 60 'kc "$K/api/v1/statuses/$k_post/context" | grep -q "a PrivaPub reply $run"' \ && ok "alice's reply threads under ktuser's post" || ko "alice's reply never reached Ktistec" kc -o /dev/null -X POST "$K/api/v1/statuses" -d "status=@alice_ktistec@privapub.test a Ktistec reply $run&in_reply_to_id=$p_post_on_k&visibility=public" until_true 60 'p_replies_have "$p_post_id" "a Ktistec reply $run"' && ok "ktuser's reply threads under alice's post" || ko "ktuser's reply missing on PrivaPub" echo " likes and boosts" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$k_post_on_p/favourite" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$k_post_on_p/reblog" until_true 60 '[ "$(k_by_alice "$k_post_uri")" = "2" ]' && ok "alice's like and boost land on Ktistec" || ko "alice's like or boost never reached Ktistec" kc -o /dev/null -X POST "$K/api/v1/statuses/$p_post_on_k/favourite" kc -o /dev/null -X POST "$K/api/v1/statuses/$p_post_on_k/reblog" until_true 60 '[ "$(p_status "$p_post_id" favourites_count) $(p_status "$p_post_id" reblogs_count)" = "1 1" ]' \ && ok "ktuser's like and boost count on PrivaPub" || ko "ktuser's like or boost not counted on PrivaPub" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$k_post_on_p/unfavourite" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$k_post_on_p/unreblog" until_true 60 '[ "$(k_by_alice "$k_post_uri")" = "0" ]' && ok "alice's undos reach Ktistec" || ko "alice's undos not applied on Ktistec" kc -o /dev/null -X POST "$K/api/v1/statuses/$p_post_on_k/unfavourite" kc -o /dev/null -X POST "$K/api/v1/statuses/$p_post_on_k/unreblog" until_true 60 '[ "$(p_status "$p_post_id" favourites_count) $(p_status "$p_post_id" reblogs_count)" = "0 0" ]' \ && ok "ktuser's undos reach PrivaPub" || ko "ktuser's undos not applied on PrivaPub" echo " polls" kn "{\"type\":\"Publish\",\"content\":\"tea or coffee, Ktistec asks $run\",\"poll-options\":[\"tea\",\"coffee\"],\"poll-duration\":\"3600\",\"visibility\":\"public\"}" >/dev/null until_true 60 '[ "$(curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next((len(s[\"poll\"][\"options\"]) for s in d if s[\"poll\"] and \"Ktistec asks $run\" in s[\"content\"]), 0))")" = "2" ]' \ && ok "ktuser's poll reaches alice as a poll" || ko "ktuser's poll never reached alice as one" k_poll_on_p=$(curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next((s['poll']['id'] for s in d if s['poll'] and 'Ktistec asks $run' in s['content']), ''))") curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/polls/$k_poll_on_p/votes" -d 'choices[]=1' # (Ktistec counts a poll of its own from the votes it keeps, which its API does not show) until_true 60 '[ "$(kt_sql "select v.name from objects v join objects q on q.iri = v.in_reply_to_iri where v.special = '"'vote'"' and v.attributed_to_iri = '"'$alice_uri'"' and q.content like '"'%Ktistec asks $run%'"'")" = "coffee" ]' \ && ok "alice's vote counts on Ktistec" || ko "alice's vote never counted on Ktistec" echo " quotes" a_quote=$(curl -s -X POST -H "$PH" "$P/api/v1/statuses" -d "status=quoting Ktistec $run&visibility=public"ed_status_id=$k_post_on_p" | j "print(d['id'])") until_true 60 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$a_quote" | j "print((d.get(\"quote\") or {}).get(\"state\"))")" = "accepted" ]' \ && ok "Ktistec approves alice's quote (FEP-044f)" || ko "alice's quote of a Ktistec post not approved ($(curl -s -H "$PH" "$P/api/v1/statuses/$a_quote" | j "print(d.get('quote'))"))" kc -o /dev/null -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' "$K/remote/objects/$p_post_on_k/quote" \ -d "{\"content\":\"Ktistec quotes PrivaPub $run\",\"quote\":\"$p_post_uri\",\"visibility\":\"public\"}" until_true 60 '[ "$(curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next((((s.get(\"quote\") or {}).get(\"quoted_status\") or {}).get(\"id\") for s in d if \"Ktistec quotes PrivaPub $run\" in s[\"content\"]), None))")" = "$p_post_id" ]' \ && ok "PrivaPub approves ktuser's quote, which reaches alice quoting her post" || ko "ktuser's quote of alice's post missing or unapproved" echo " edits and deletions" kn "{\"type\":\"Publish\",\"object\":\"$k_post_uri\",\"content\":\"a Ktistec post $run, edited\",\"visibility\":\"public\"}" >/dev/null until_true 60 'p_status "$k_post_on_p" content | grep -q "edited"' && ok "ktuser's edit reaches PrivaPub" || ko "ktuser's edit never reached PrivaPub" curl -s -o /dev/null -X PUT -H "$PH" "$P/api/v1/statuses/$p_post_id" -d "status=a PrivaPub post for Ktistec $run, edited" until_true 60 'k_status "$p_post_on_k" content | grep -q "edited"' && ok "alice's edit reaches Ktistec" || ko "alice's edit never reached Ktistec" kn "{\"type\":\"Delete\",\"object\":\"$k_post_uri\"}" >/dev/null until_true 60 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$k_post_on_p")" = "404" ]' \ && ok "ktuser's deletion reaches PrivaPub" || ko "ktuser's deleted post still on PrivaPub" curl -s -o /dev/null -X DELETE -H "$PH" "$P/api/v1/statuses/$p_post_id" until_true 60 '[ "$(kt_sql "select count(*) from objects where iri = '"'$p_post_uri'"' and deleted_at is null")" = "0" ]' \ && ok "alice's deletion reaches Ktistec" || ko "alice's deleted post still on Ktistec" echo " unfollow" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$kt_on_p/unfollow" until_true 60 '[ "$(kt_sql "select count(*) from relationships where type = '"'Relationship::Social::Follow'"' and from_iri = '"'$alice_uri'"' and to_iri = '"'$kt_uri'"'")" = "0" ]' \ && ok "alice's unfollow reaches Ktistec" || ko "Ktistec still counts alice following ktuser" echo " statistics" stats_check ktistec.test ktistec