# Misskey 2026.10 (or Sharkey, with MISSKEY_NAME=sharkey): discovery, follows, posts and CW, replies both ways, Misskey # reactions both ways, legacy quotes both ways, MFM source, polls, specified notes, media, deletes, blocks, statistics. # A delivery is never checked with ap/show, which would fetch it from us; only with what Misskey already holds. MK_NAME=${MISSKEY_NAME:-misskey} MKH="$MK_NAME.test" MK="https://$MKH:6443" MKT=$(cat "$here/.state/$MK_NAME.token" 2>/dev/null) # mk [json]: a Misskey API call as mkuser mk() { local body=${2:-'{}'} site "$MKH" -s -X POST "$MK/api/$1" -H 'Content-Type: application/json' \ -d "$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); d['i']=sys.argv[2]; print(json.dumps(d))" "$body" "$MKT")" } # the notes Misskey holds of an account, and the one whose uri is given mk_notes() { mk users/notes "{\"userId\":\"$1\",\"limit\":50,\"withReplies\":true,\"withRenotes\":true}"; } # users/relation answers a list even for one user mk_relation() { mk users/relation "{\"userId\":\"$1\"}" | j "r=d[0] if isinstance(d,list) else d; print(r['$2'])"; } mk_note_by_uri() { mk_notes "$1" | j "print(json.dumps(next((n for n in d if n.get('uri')=='$2'), None)))"; } echo "$MK_NAME" [ -n "$MKT" ] && ok "$MK_NAME token for mkuser" || { ko "$MK_NAME token"; return 1; } KT=$(privapub_token "alice_$MK_NAME") KH="Authorization: Bearer $KT" [ -n "$KT" ] && ok "PrivaPub token for alice_$MK_NAME" || { ko "PrivaPub token for alice_$MK_NAME"; return 1; } echo " discovery" alice_on_mk=$(mk users/show "{\"username\":\"alice_$MK_NAME\",\"host\":\"privapub.test\"}" | j "print(d['id'])") [ -n "$alice_on_mk" ] && ok "$MK_NAME resolves @alice_$MK_NAME@privapub.test" || ko "$MK_NAME cannot resolve alice" mk_on_p=$(curl -s -H "$KH" "$P/api/v2/search?q=mkuser@$MKH&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") [ -n "$mk_on_p" ] && ok "PrivaPub resolves @mkuser@$MKH" || ko "PrivaPub cannot resolve mkuser" echo " follows" mk following/create "{\"userId\":\"$alice_on_mk\"}" >/dev/null until_true 30 '[ "$(mk_relation "$alice_on_mk" isFollowing)" = "True" ]' && ok "mkuser follows alice (Accept arrived)" || ko "$MK_NAME's follow not accepted" curl -s -o /dev/null -X POST -H "$KH" "$P/api/v1/accounts/$mk_on_p/follow" until_true 30 '[ "$(curl -s -H "$KH" "$P/api/v1/accounts/relationships?id[]=$mk_on_p" | j "print(d[0][\"following\"])")" = "True" ]' && ok "alice follows mkuser (Accept arrived)" || ko "PrivaPub's follow not accepted" echo " posts" k_post=$(curl -s -X POST -H "$KH" $P/api/v1/statuses -d "status=Hello $MK_NAME from PrivaPub&visibility=public") k_post_id=$(echo "$k_post" | j "print(d['id'])"); k_post_uri=$(echo "$k_post" | j "print(d['uri'])") until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$k_post_uri")" != "null" ]' && ok "alice's post reaches $MK_NAME" || ko "post missing on $MK_NAME" k_post_on_mk=$(mk_note_by_uri "$alice_on_mk" "$k_post_uri" | j "print(d['id'])") mk_post=$(mk notes/create "{\"text\":\"Hello PrivaPub from $MK_NAME, \$[x2 big] words\",\"visibility\":\"public\"}" | j "print(d['createdNote']['id'])") until_true 30 'curl -s -H "$KH" "$P/api/v1/timelines/home" | grep -q "Hello PrivaPub from $MK_NAME"' && ok "mkuser's post reaches alice's home" || ko "$MK_NAME's post missing on PrivaPub" mk_post_on_p=$(curl -s -H "$KH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if 'Hello PrivaPub from $MK_NAME' in s['content']))") [ "$(curl -s -H "$KH" "$P/api/v1/statuses/$mk_post_on_p" | j "print('\$[x2' in ((d.get('privapub') or {}).get('source') or {}).get('content',''))")" = "True" ] \ && ok "the MFM source is kept" || ko "MFM source lost" cw_uri=$(curl -s -X POST -H "$KH" $P/api/v1/statuses -d 'status=behind a warning&spoiler_text=spoilers&visibility=public' | j "print(d['uri'])") until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$cw_uri" | j "print(d and d[\"cw\"])")" = "spoilers" ]' && ok "a content warning survives to $MK_NAME" || ko "content warning lost on $MK_NAME" echo " replies" mk notes/create "{\"text\":\"@alice_$MK_NAME@privapub.test replying from $MK_NAME\",\"replyId\":\"$k_post_on_mk\",\"visibility\":\"public\"}" >/dev/null until_true 30 'curl -s -H "$KH" "$P/api/v1/notifications" | j "print(any(n[\"type\"]==\"mention\" for n in d))" | grep -q True' && ok "mkuser's reply notifies alice" || ko "reply did not notify" until_true 15 '[ "$(curl -s -H "$KH" "$P/api/v1/statuses/$k_post_id/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]' && ok "the reply threads under alice's post" || ko "reply not threaded on PrivaPub" k_reply_uri=$(curl -s -X POST -H "$KH" $P/api/v1/statuses -d "status=@mkuser@$MKH replying from PrivaPub&in_reply_to_id=$mk_post_on_p&visibility=public" | j "print(d['uri'])") until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$k_reply_uri" | j "print(d and d[\"replyId\"])")" = "$mk_post" ]' && ok "alice's reply threads under mkuser's note" || ko "outbound reply not threaded on $MK_NAME" echo " reactions" mk notes/reactions/create "{\"noteId\":\"$k_post_on_mk\",\"reaction\":\"👍\"}" >/dev/null until_true 30 'curl -s -H "$KH" "$P/api/v1/pleroma/statuses/$k_post_id/reactions" | j "print(any(r[\"name\"]==\"👍\" and r[\"count\"]==1 for r in d))" | grep -q True' && ok "mkuser's 👍 reaction reaches PrivaPub" || ko "reaction missing on PrivaPub" mk notes/reactions/delete "{\"noteId\":\"$k_post_on_mk\"}" >/dev/null until_true 30 '[ "$(curl -s -H "$KH" "$P/api/v1/pleroma/statuses/$k_post_id/reactions" | j "print(len(d))")" = "0" ]' && ok "mkuser's withdrawn reaction leaves PrivaPub" || ko "reaction not withdrawn on PrivaPub" curl -s -o /dev/null -X PUT -H "$KH" "$P/api/v1/pleroma/statuses/$mk_post_on_p/reactions/%F0%9F%8E%89" until_true 30 '[ "$(mk notes/show "{\"noteId\":\"$mk_post\"}" | j "print(d[\"reactions\"].get(\"🎉\", 0))")" = "1" ]' && ok "alice's 🎉 reaction reaches $MK_NAME" || ko "reaction missing on $MK_NAME" # Misskey keeps one reaction per user and note, so the like goes to a note of its own mk_liked=$(mk notes/create "{\"text\":\"like this one, PrivaPub\",\"visibility\":\"public\"}" | j "print(d['createdNote']['id'])") until_true 30 'curl -s -H "$KH" "$P/api/v1/timelines/home" | grep -q "like this one, PrivaPub"' || true mk_liked_on_p=$(curl -s -H "$KH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if 'like this one, PrivaPub' in s['content']))") curl -s -o /dev/null -X POST -H "$KH" "$P/api/v1/statuses/$mk_liked_on_p/favourite" until_true 30 '[ "$(mk notes/show "{\"noteId\":\"$mk_liked\"}" | j "print(sum(d[\"reactions\"].values()))")" = "1" ]' && ok "alice's like counts as a reaction on $MK_NAME" || ko "like not counted on $MK_NAME" echo " quotes" mk_quote=$(mk notes/create "{\"text\":\"quoting PrivaPub\",\"renoteId\":\"$k_post_on_mk\",\"visibility\":\"public\"}" | j "print(d['createdNote']['id'])") until_true 30 'curl -s -H "$KH" "$P/api/v1/timelines/home" | j "print(any(\"quoting PrivaPub\" in s[\"content\"] and (s.get(\"quote\") or {}).get(\"state\")==\"accepted\" for s in d))" | grep -q True' \ && ok "mkuser's quote arrives as an accepted quote" || ko "$MK_NAME's quote not understood" k_quote_uri=$(curl -s -X POST -H "$KH" $P/api/v1/statuses -d "status=quoting $MK_NAME&visibility=public"ed_status_id=$mk_post_on_p" | j "print(d['uri'])") until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$k_quote_uri" | j "print(d and d[\"renoteId\"])")" = "$mk_post" ]' && ok "alice's quote is a quote on $MK_NAME" || ko "quote not understood by $MK_NAME" echo " polls" k_poll_uri=$(curl -s -X POST -H "$KH" $P/api/v1/statuses -d 'status=cats or dogs&visibility=public&poll[options][]=cats&poll[options][]=dogs&poll[expires_in]=3600' | j "print(d['uri'])") until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$k_poll_uri" | j "print(len(d[\"poll\"][\"choices\"]))")" = "2" ]' && ok "alice's poll reaches $MK_NAME as a poll" || ko "poll missing on $MK_NAME" k_poll_on_mk=$(mk_note_by_uri "$alice_on_mk" "$k_poll_uri" | j "print(d['id'])") mk notes/polls/vote "{\"noteId\":\"$k_poll_on_mk\",\"choice\":1}" >/dev/null alice_self=$(curl -s -H "$KH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])") until_true 30 '[ "$(curl -s -H "$KH" "$P/api/v1/accounts/$alice_self/statuses" | j "print(next(s[\"poll\"][\"options\"][1][\"votes_count\"] for s in d if s[\"uri\"]==\"$k_poll_uri\"))")" = "1" ]' && ok "mkuser's vote counts on PrivaPub" || ko "vote not counted on PrivaPub" mk_poll=$(mk notes/create "{\"text\":\"tea or coffee, $MK_NAME asks\",\"visibility\":\"public\",\"poll\":{\"choices\":[\"tea\",\"coffee\"],\"expiredAfter\":3600000}}" | j "print(d['createdNote']['id'])") until_true 30 'curl -s -H "$KH" "$P/api/v1/timelines/home" | j "print(any(s[\"poll\"] and \"$MK_NAME asks\" in s[\"content\"] for s in d))" | grep -q True' && ok "mkuser's poll reaches PrivaPub as a poll" || ko "poll missing on PrivaPub" mk_poll_on_p=$(curl -s -H "$KH" "$P/api/v1/timelines/home" | j "print(next(s['poll']['id'] for s in d if s['poll'] and '$MK_NAME asks' in s['content']))") curl -s -o /dev/null -X POST -H "$KH" "$P/api/v1/polls/$mk_poll_on_p/votes" -d 'choices[]=0' until_true 30 '[ "$(mk notes/show "{\"noteId\":\"$mk_poll\"}" | j "print(d[\"poll\"][\"choices\"][0][\"votes\"])")" = "1" ]' && ok "alice's vote counts on $MK_NAME" || ko "vote not counted on $MK_NAME" echo " specified notes" mk notes/create "{\"text\":\"@alice_$MK_NAME@privapub.test a secret for PrivaPub\",\"visibility\":\"specified\",\"visibleUserIds\":[\"$alice_on_mk\"]}" >/dev/null until_true 30 'curl -s -H "$KH" "$P/api/v1/conversations" | grep -q "a secret for PrivaPub"' && ok "mkuser's specified note arrives as a DM" || ko "specified note missing on PrivaPub" curl -s -o /dev/null -X POST -H "$KH" $P/api/v1/statuses -d "status=@mkuser@$MKH a secret for $MK_NAME&visibility=direct" until_true 30 'mk notes/mentions "{\"visibility\":\"specified\"}" | grep -q "a secret for $MK_NAME"' && ok "alice's DM arrives as a specified note" || ko "DM missing on $MK_NAME" echo " media" make_png "$work/red.png" k_media=$(curl -s -X POST -H "$KH" "$P/api/v2/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square' | j "print(d['id'])") k_media_uri=$(curl -s -X POST -H "$KH" $P/api/v1/statuses -d "status=a picture for $MK_NAME&visibility=public&media_ids[]=$k_media" | j "print(d['uri'])") until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$k_media_uri" | j "print(d[\"files\"][0][\"comment\"])")" = "a red square" ]' && ok "an image with alt text reaches $MK_NAME" || ko "image or alt text missing on $MK_NAME" mk_file=$(site "$MKH" -s -X POST "$MK/api/drive/files/create" -F "i=$MKT" -F "file=@$work/red.png;type=image/png" -F "comment=a red square from $MK_NAME" | j "print(d['id'])") mk notes/create "{\"text\":\"a picture from $MK_NAME\",\"visibility\":\"public\",\"fileIds\":[\"$mk_file\"]}" >/dev/null until_true 30 'curl -s -H "$KH" "$P/api/v1/timelines/home" | j "print(any(\"a picture from $MK_NAME\" in s[\"content\"] and s[\"media_attachments\"] and \"/media/proxy/\" in s[\"media_attachments\"][0][\"url\"] and s[\"media_attachments\"][0][\"description\"]==\"a red square from $MK_NAME\" for s in d))" | grep -q True' \ && ok "an image with alt text from $MK_NAME arrives through our proxy" || ko "$MK_NAME's image missing, unproxied or without alt text" echo " deletes" cw_on_mk=$(mk_note_by_uri "$alice_on_mk" "$cw_uri" | j "print(d['id'])") cw_id=$(curl -s -H "$KH" "$P/api/v1/accounts/$alice_self/statuses" | j "print(next(s['id'] for s in d if s['uri']=='$cw_uri'))") curl -s -o /dev/null -X DELETE -H "$KH" "$P/api/v1/statuses/$cw_id" until_true 30 '[ "$(mk notes/show "{\"noteId\":\"$cw_on_mk\"}" | j "print(d.get(\"error\",{}).get(\"code\"))")" = "NO_SUCH_NOTE" ]' && ok "alice's delete reaches $MK_NAME" || ko "delete not applied on $MK_NAME" mk notes/delete "{\"noteId\":\"$mk_poll\"}" >/dev/null until_true 30 '! curl -s -H "$KH" "$P/api/v1/timelines/home" | grep -q "$MK_NAME asks"' && ok "mkuser's delete reaches PrivaPub" || ko "$MK_NAME's delete not applied on PrivaPub" echo " unfollows and blocks" curl -s -o /dev/null -X POST -H "$KH" "$P/api/v1/accounts/$mk_on_p/unfollow" until_true 30 '[ "$(mk_relation "$alice_on_mk" isFollowed)" = "False" ]' && ok "alice's unfollow reaches $MK_NAME" || ko "unfollow not applied on $MK_NAME" curl -s -o /dev/null -X POST -H "$KH" "$P/api/v1/accounts/$mk_on_p/block" until_true 30 '[ "$(mk_relation "$alice_on_mk" isBlocked)" = "True" ]' && ok "alice's block reaches $MK_NAME" || ko "block not applied on $MK_NAME" curl -s -o /dev/null -X POST -H "$KH" "$P/api/v1/accounts/$mk_on_p/unblock" echo " statistics" stats_check "$MKH" "$MK_NAME"