R8 the Mastodon API made real: featured tags, endorsements, notes, notify and languages, suggestions, the direct stream
Featured tags per persona (/api/v1/featured_tags, suggestions, tags/:name/feature, accounts' featured tags, max 10),
listed at /tattoos as Hashtags and sent as Add/Remove{Hashtag} to followers on the featured collection, as Mastodon
sends them; remote accounts' tags received and read from featuredTags. Endorsements (pin/endorse) and private notes
(PersonaNote) kept per persona and never federated; the relationship's notifying, languages, endorsed and note, with
follow's notify (status notifications) and languages (home filter). Reading preferences set on /clientapi. Suggestions
v1/v2 with dismissals and staff picks, never the persona or a sibling. conversation events on the direct stream.
translate, annual reports and the admin API answer. New collections backed up and removed with their persona.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
d08fbabaa5
commit
87529bca49
42 files changed
+1505
-77
No files matched your search
@@ -0,0 +1,128 @@
|
||||
# The Mastodon API made real (completion program R8), with Mastodon. mastouser follows feat_alice, who features a hashtag:
|
||||
# her /tattoos serves it and Mastodon shows it among her account's featured tags (her Add{Hashtag}), and unfeaturing it
|
||||
# takes it away there (Remove{Hashtag}). feat_alice follows mastouser with notify=true, and a tag he features shows among
|
||||
# his featured tags here; his next post gives her a `status` notification; his direct message to her is told on her
|
||||
# `direct` stream, as server-sent events and on the WebSocket, as a `conversation` event. Her endorsement of him and her
|
||||
# private note on him are hers alone: feat_bob, a sibling persona of the same login, sees neither, nobody else is told her
|
||||
# endorsements, and feat_bob is never suggested to her, though she favourited his post. The stubs answer: translate 422,
|
||||
# annual reports empty, the Mastodon admin API 403. Needs the mastodon peer (batch A); run after emoji.
|
||||
M=https://mastodon.test:6443
|
||||
mcurl() { curl -sk --resolve mastodon.test:6443:127.0.0.1 "$@"; }
|
||||
. "$here/peers/mastodon.sh"
|
||||
code() { curl -s -o /dev/null -w '%{http_code}' "$@"; }
|
||||
# m_signed_body <url>: one of our documents as Mastodon's instance actor reads it, signed (SecureMode answers unsigned 401)
|
||||
m_signed_body() { podman exec pasture-mastodon bin/rails runner "Request.new(:get, '$1').on_behalf_of(Account.representative).add_headers('Accept' => 'application/activity+json').perform { |r| puts r.body_with_limit }" 2>/dev/null | tail -1; }
|
||||
m_featured() { mcurl -H "$MH" "$M/api/v1/accounts/$1/featured_tags" | j "print(any((t.get('name') or '').lower() == '$2' for t in d or []))"; }
|
||||
p_featured() { curl -s -H "$AH" "$P/api/v1/accounts/$1/featured_tags" | j "print(any((t.get('name') or '').lower() == '$2' for t in d or []))"; }
|
||||
|
||||
echo "featured"
|
||||
run=$(date +%s)
|
||||
tag="pasture$run"; mtag="mastopasture$run"
|
||||
AT=$(privapub_token feat_alice); AH="Authorization: Bearer $AT"
|
||||
BT=$(privapub_token feat_bob); BH="Authorization: Bearer $BT"
|
||||
MT=$(mastodon_token); MH="Authorization: Bearer $MT"
|
||||
[ -n "$AT" ] && [ -n "$BT" ] && [ -n "$MT" ] && ok "tokens for feat_alice, feat_bob (her sibling, the same login) and mastouser" \
|
||||
|| { ko "a token is missing"; return 1; }
|
||||
alice_id=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
||||
bob_id=$(curl -s -H "$BH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
||||
|
||||
echo " follows"
|
||||
alice_on_m=$(mcurl -H "$MH" "$M/api/v2/search?q=@feat_alice@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
||||
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/accounts/$alice_on_m/follow"
|
||||
until_true 30 '[ "$(curl -s "$P/api/v1/accounts/$alice_id" | j "print(d[\"followers_count\"])")" -ge 1 ]' \
|
||||
&& ok "mastouser follows feat_alice" || ko "mastouser's follow never arrived"
|
||||
m_on_p=$(curl -s -H "$AH" "$P/api/v2/search?q=mastouser@mastodon.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
||||
rel=$(curl -s -X POST -H "$AH" "$P/api/v1/accounts/$m_on_p/follow" -d notify=true -d 'languages[]=en')
|
||||
[ "$(echo "$rel" | j "print(d['notifying'] is True and d['languages'] == ['en'])")" = True ] \
|
||||
&& ok "feat_alice follows mastouser with notify and languages (the relationship says so)" || ko "the follow's options: $(echo "$rel" | head -c 300)"
|
||||
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/accounts/relationships?id[]=$m_on_p" | j "print(d[0][\"following\"])")" = True ]' \
|
||||
&& ok "mastouser accepts feat_alice" || ko "feat_alice's follow was not accepted"
|
||||
|
||||
echo " her featured tag"
|
||||
featured=$(curl -s -X POST -H "$AH" "$P/api/v1/featured_tags" -d "name=$tag")
|
||||
featured_id=$(echo "$featured" | j "print(d['id'])")
|
||||
[ "$(echo "$featured" | j "print(d['name'] == '$tag' and d['statuses_count'] == '0')")" = True ] && ok "feat_alice features #$tag" \
|
||||
|| ko "featuring was refused: $(echo "$featured" | head -c 300)"
|
||||
[ "$(curl -s -H "$AH" "$P/api/v1/featured_tags" | j "print(any(t['id'] == '$featured_id' for t in d))")" = True ] \
|
||||
&& [ "$(curl -s "$P/api/v1/accounts/$alice_id/featured_tags" | j "print(any(t['name'] == '$tag' for t in d))")" = True ] \
|
||||
&& ok "her featured tags list it, anonymously too" || ko "her featured tags miss it"
|
||||
[ "$(curl -s "$P/api/v2/instance" | j "print(d['configuration']['accounts']['max_featured_tags'])")" = 10 ] \
|
||||
&& ok "the instance API allows ten" || ko "max_featured_tags: $(curl -s "$P/api/v2/instance" | j "print(d['configuration']['accounts'])")"
|
||||
[ "$(m_signed_body https://privapub.test/peasants/feat_alice/tattoos | j "print(any(i['type'] == 'Hashtag' and i['name'] == '#$tag' for i in d['orderedItems']))")" = True ] \
|
||||
&& ok "/tattoos serves it as a Hashtag (signed)" || ko "/tattoos: $(m_signed_body https://privapub.test/peasants/feat_alice/tattoos | head -c 300)"
|
||||
until_true 30 '[ "$(m_featured "$alice_on_m" "$tag")" = True ]' \
|
||||
&& ok "Mastodon shows #$tag among feat_alice's featured tags" || ko "Mastodon's featured tags of feat_alice: $(mcurl -H "$MH" "$M/api/v1/accounts/$alice_on_m/featured_tags" | head -c 300)"
|
||||
[ "$(code -X DELETE -H "$AH" "$P/api/v1/featured_tags/$featured_id")" = 200 ] && ok "feat_alice unfeatures it" || ko "unfeaturing was refused"
|
||||
until_true 30 '[ "$(m_featured "$alice_on_m" "$tag")" = False ]' \
|
||||
&& ok "Mastodon no longer shows it" || ko "Mastodon still shows #$tag"
|
||||
|
||||
echo " his featured tag"
|
||||
m_featured_id=$(mcurl -X POST -H "$MH" "$M/api/v1/featured_tags" -d "name=$mtag" | j "print(d['id'])")
|
||||
[ -n "$m_featured_id" ] && ok "mastouser features #$mtag" || ko "Mastodon refused the featured tag"
|
||||
until_true 30 '[ "$(p_featured "$m_on_p" "$mtag")" = True ]' \
|
||||
&& ok "feat_alice reads #$mtag among mastouser's featured tags" || ko "his featured tags here: $(curl -s -H "$AH" "$P/api/v1/accounts/$m_on_p/featured_tags" | head -c 300)"
|
||||
mcurl -o /dev/null -X DELETE -H "$MH" "$M/api/v1/featured_tags/$m_featured_id"
|
||||
until_true 30 '[ "$(p_featured "$m_on_p" "$mtag")" = False ]' \
|
||||
&& ok "his unfeaturing reaches PrivaPub" || ko "#$mtag is still featured here"
|
||||
|
||||
echo " notify"
|
||||
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses" --data-urlencode "status=notify me, PrivaPub $run" -d visibility=public -d language=en
|
||||
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/notifications?types[]=status" | j "print(any(\"notify me, PrivaPub $run\" in n[\"status\"][\"content\"] for n in d))")" = True ]' \
|
||||
&& ok "his post gives feat_alice a status notification" || ko "no status notification: $(curl -s -H "$AH" "$P/api/v1/notifications?limit=5" | head -c 300)"
|
||||
|
||||
echo " the direct stream"
|
||||
curl -s -N -H "$AH" "$P/api/v1/streaming/direct" > "$work/sse.txt" & sse=$!
|
||||
cat > "$work/ws.py" <<'PY'
|
||||
import asyncio, json, sys, websockets
|
||||
async def main(url, token, seconds):
|
||||
async with websockets.connect(url, subprotocols=[token]) as ws:
|
||||
await ws.send(json.dumps({"type": "subscribe", "stream": "direct"}))
|
||||
try:
|
||||
while True:
|
||||
message = json.loads(await asyncio.wait_for(ws.recv(), timeout=seconds))
|
||||
print(json.dumps(message), flush=True)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
asyncio.run(main(sys.argv[1], sys.argv[2], float(sys.argv[3])))
|
||||
PY
|
||||
python3 -I "$work/ws.py" "ws://127.0.0.1:${PASTURE_PORT:-6971}/api/v1/streaming" "$AT" 40 > "$work/ws.txt" 2>&1 & ws=$!
|
||||
sleep 3
|
||||
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses" --data-urlencode "status=@feat_alice@privapub.test a secret, PrivaPub $run" -d visibility=direct
|
||||
until_true 30 'grep -q "^event: conversation" "$work/sse.txt"' && ok "the SSE direct stream tells a conversation event" \
|
||||
|| ko "the SSE direct stream: $(head -c 300 "$work/sse.txt")"
|
||||
until_true 20 '[ "$(python3 -c "import json,sys
|
||||
for l in open(sys.argv[1]):
|
||||
m=json.loads(l)
|
||||
if m.get(\"event\")==\"conversation\" and m.get(\"stream\")==[\"direct\"] and \"a secret, PrivaPub $run\" in json.loads(m[\"payload\"])[\"last_status\"][\"content\"]: print(True); break" "$work/ws.txt" 2>/dev/null)" = True ]' \
|
||||
&& ok "the WebSocket's direct subscription tells it, with the message" || ko "the WebSocket: $(head -c 300 "$work/ws.txt")"
|
||||
kill "$sse" "$ws" 2>/dev/null; wait "$sse" "$ws" 2>/dev/null
|
||||
|
||||
echo " endorsements and notes stay hers"
|
||||
[ "$(curl -s -X POST -H "$AH" "$P/api/v1/accounts/$m_on_p/endorse" | j "print(d['endorsed'])")" = True ] \
|
||||
&& [ "$(curl -s -H "$AH" "$P/api/v1/endorsements" | j "print(any(a['id'] == '$m_on_p' for a in d))")" = True ] \
|
||||
&& ok "feat_alice endorses mastouser" || ko "the endorsement did not hold"
|
||||
[ "$(curl -s -X POST -H "$AH" "$P/api/v1/accounts/$m_on_p/note" --data-urlencode "comment=met in the pasture $run" | j "print(d['note'])")" = "met in the pasture $run" ] \
|
||||
&& ok "her private note on him" || ko "the note was not kept"
|
||||
[ "$(curl -s -H "$BH" "$P/api/v1/accounts/relationships?id[]=$m_on_p" | j "print(d[0]['note'] == '' and d[0]['endorsed'] is False)")" = True ] \
|
||||
&& [ "$(curl -s -H "$BH" "$P/api/v1/endorsements" | j "print(d == [])")" = True ] \
|
||||
&& ok "feat_bob, of the same login, sees neither her note nor her endorsement" || ko "a sibling sees her note or endorsement"
|
||||
[ "$(curl -s -H "$BH" "$P/api/v1/accounts/$alice_id/endorsements" | j "print(d == [])")" = True ] \
|
||||
&& [ "$(curl -s "$P/api/v1/accounts/$alice_id/endorsements" | j "print(d == [])")" = True ] \
|
||||
&& [ "$(curl -s -H "$AH" "$P/api/v1/accounts/$alice_id/endorsements" | j "print(any(a['id'] == '$m_on_p' for a in d))")" = True ] \
|
||||
&& ok "her endorsements are told to her alone" || ko "her endorsements are told to others"
|
||||
bob_post=$(curl -s -X POST -H "$BH" "$P/api/v1/statuses" --data-urlencode "status=bob's own post $run" -d visibility=public | j "print(d['id'])")
|
||||
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/statuses/$bob_post/favourite"
|
||||
[ "$(curl -s -H "$AH" "$P/api/v2/suggestions?limit=80" | j "print(all(s['account']['id'] not in ('$bob_id', '$alice_id') for s in d))")" = True ] \
|
||||
&& ok "feat_bob is never suggested to her, though she favourited his post" || ko "a sibling is suggested: $(curl -s -H "$AH" "$P/api/v2/suggestions" | head -c 300)"
|
||||
[ "$(curl -s -H "$AH" "$P/api/v1/accounts/familiar_followers?id[]=$m_on_p" | j "print(d == [{'id': '$m_on_p', 'accounts': []}])")" = True ] \
|
||||
&& ok "familiar followers stay empty" || ko "familiar followers: $(curl -s -H "$AH" "$P/api/v1/accounts/familiar_followers?id[]=$m_on_p")"
|
||||
|
||||
echo " stubs"
|
||||
[ "$(code -X POST -H "$AH" "$P/api/v1/statuses/$bob_post/translate")" = 422 ] && ok "translate answers 422" || ko "translate answers otherwise"
|
||||
[ "$(curl -s -H "$AH" "$P/api/v1/annual_reports" | j "print(d == {'annual_reports': [], 'accounts': [], 'statuses': []})")" = True ] \
|
||||
&& ok "annual reports answer none" || ko "annual reports: $(curl -s -H "$AH" "$P/api/v1/annual_reports")"
|
||||
[ "$(code -H "$AH" "$P/api/v1/admin/accounts")" = 403 ] && [ "$(curl -s "$P/api/v1/admin/reports" | j "print(d['error'])")" = "This action is not allowed" ] \
|
||||
&& ok "the Mastodon admin API answers 403" || ko "the admin API answers $(code -H "$AH" "$P/api/v1/admin/accounts")"
|
||||
|
||||
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$m_on_p/unendorse"
|
||||
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$m_on_p/note" -d comment=
|
||||
Reference in new issue
Block a user