Build / Build (push) Successful in 1m7s
The post check handed the whole status to has_code instead of its emojis, and Misskey lists its reactions by name
(':code@host:'), which has_code took for objects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
140 lines
14 KiB
Bash
140 lines
14 KiB
Bash
# Custom emoji (completion program R7). The administrator adds one of the server's own emoji through
|
|
# /clientapi/admin/emoji, which the Mastodon API lists and /grimaces/{shortcode} serves as an ActivityPub Emoji (signed or
|
|
# not); a public post using it reaches Mastodon, GoToSocial, Misskey and Akkoma with the emoji (their APIs list it among
|
|
# the status's emojis); a display name using it shows it on Mastodon; an EmojiReact with it reaches Akkoma and Misskey
|
|
# as that emoji; deleting it takes it out of the list and answers 410 at its id, and a post written after names it no
|
|
# more. The staff's log keeps the insert and the delete. Needs the gts, mastodon, misskey and akkoma peers (batch A);
|
|
# run it after moderation, as every scenario that edits the server as its administrator.
|
|
G=https://gts.test:6443
|
|
gcurl() { curl -sk --resolve gts.test:6443:127.0.0.1 "$@"; }
|
|
M=https://mastodon.test:6443
|
|
mcurl() { curl -sk --resolve mastodon.test:6443:127.0.0.1 "$@"; }
|
|
AK=https://akkoma.test:6443
|
|
acurl() { curl -sk --resolve akkoma.test:6443:127.0.0.1 "$@"; }
|
|
MK=https://misskey.test:6443
|
|
MKT=$(cat "$here/.state/misskey.token" 2>/dev/null)
|
|
KT=$(cat "$here/.state/akkoma.token" 2>/dev/null); KH="Authorization: Bearer $KT"
|
|
. "$here/peers/mastodon.sh"
|
|
# mk <endpoint> [json]: a Misskey API call as mkuser
|
|
mk() {
|
|
local body=${2:-'{}'}
|
|
site misskey.test -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")"
|
|
}
|
|
mk_note_by_uri() { mk users/notes "{\"userId\":\"$1\",\"limit\":50,\"withReplies\":true}" | j "print(json.dumps(next((n for n in d if n.get('uri')=='$2'), None)))"; }
|
|
login() { curl -s -X POST "$P/clientapi/user/login" -H 'Content-Type: application/json' -d "{\"userName\":\"$1\",\"password\":\"$2\"}" | j "print(d['token'])"; }
|
|
code_of() { local jwt=$1 method=$2 path=$3; shift 3; curl -s -o /dev/null -w '%{http_code}' -X "$method" -H "Authorization: Bearer $jwt" "$P$path" "$@"; }
|
|
gts_token() {
|
|
local app id secret code jar
|
|
jar=$(mktemp); app=$(gcurl -X POST $G/api/v1/apps -d 'client_name=pasture&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=read+write+follow')
|
|
id=$(echo "$app" | j "print(d['client_id'])"); secret=$(echo "$app" | j "print(d['client_secret'])")
|
|
gcurl -o /dev/null -c "$jar" -b "$jar" "$G/oauth/authorize?client_id=$id&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=read+write+follow"
|
|
gcurl -o /dev/null -c "$jar" -b "$jar" -X POST $G/auth/sign_in --data-urlencode 'username=gtsuser@gts.test' --data-urlencode 'password=Gts-Pasture-Pass-1!'
|
|
code=$(gcurl -o /dev/null -w '%{redirect_url}' -c "$jar" -b "$jar" -X POST "$G/oauth/authorize" | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p')
|
|
rm -f "$jar"
|
|
gcurl -X POST $G/oauth/token -d "grant_type=authorization_code&code=$code&client_id=$id&client_secret=$secret&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=read+write+follow" | j "print(d['access_token'])"
|
|
}
|
|
# GoToSocial answers a lookup by URI with resolve=false from its database alone (its home timeline can't be trusted)
|
|
on_gts() { gcurl -G -H "$GH" "$G/api/v2/search" --data-urlencode "q=$1" -d resolve=false -d type=statuses; }
|
|
# names <json of emojis>: the shortcodes a list of emoji entities, or Misskey's name-to-url map, holds (hosts left out)
|
|
has_code() { j "e=d or []
|
|
names=[(x.get('shortcode') or x.get('name')) if isinstance(x, dict) else x for x in e] if isinstance(e, list) else list(e.keys())
|
|
print(any((n or '').strip(':').split('@')[0] == '$1' for n in names))"; }
|
|
p_home_id() { curl -s -H "$AH" "$P/api/v1/timelines/home?limit=40" | j "print(next((s['id'] for s in d if '$1' in s['content']), ''))"; }
|
|
|
|
echo "emoji"
|
|
run=$(date +%s)
|
|
code="pasture_$run"
|
|
privapub_root >/dev/null
|
|
podman exec -w /app pasture-privapub /app/PrivaPub admin promote "$ROOT_USER" >/dev/null 2>&1 || true
|
|
ADMIN=$(login "$ROOT_USER" "$ROOT_PASS")
|
|
[ -n "$ADMIN" ] && ok "the pasture's root signs in as the administrator" || { ko "no administrator's login"; return 1; }
|
|
AT=$(privapub_token emoji_alice); AH="Authorization: Bearer $AT"
|
|
MT=$(mastodon_token); MH="Authorization: Bearer $MT"
|
|
GT=$(gts_token); GH="Authorization: Bearer $GT"
|
|
[ -n "$AT" ] && [ -n "$MT" ] && [ -n "$GT" ] && [ -n "$MKT" ] && [ -n "$KT" ] && ok "tokens for emoji_alice, mastouser, gtsuser, mkuser and akuser" \
|
|
|| { ko "a token is missing"; return 1; }
|
|
|
|
echo " the administrator adds it"
|
|
make_png "$work/emoji.png"
|
|
added=$(curl -s -X POST -H "Authorization: Bearer $ADMIN" "$P/clientapi/admin/emoji/insert" -F "shortcode=$code" -F 'category=Pasture' \
|
|
-F "image=@$work/emoji.png;type=image/png")
|
|
emoji_url=$(echo "$added" | j "print(d['url'])"); emoji_uri=$(echo "$added" | j "print(d['uri'])")
|
|
[ "$(echo "$added" | j "print(d['shortcode'] == '$code' and d['contentType'] == 'image/png' and d['uri'] == 'https://privapub.test/grimaces/$code')")" = True ] \
|
|
&& ok "an administrator adds :$code: (a PNG, its id /grimaces/$code)" || ko "the emoji was not added: $(echo "$added" | head -c 300)"
|
|
[ "$(code_of "$ADMIN" POST /clientapi/admin/emoji/insert -F "shortcode=$code" -F "image=@$work/emoji.png;type=image/png")" = 409 ] \
|
|
&& ok "the same shortcode is refused (409)" || ko "a second :$code: was taken"
|
|
[ "$(code_of "$AT" GET /clientapi/admin/emoji/list)" != 200 ] && ok "a persona's token can't list them" || ko "a persona read the administrator's list"
|
|
[ "$(curl -s "$P/api/v1/custom_emojis" | j "print(any(e['shortcode'] == '$code' and e['url'] == '$emoji_url' and e['category'] == 'Pasture' for e in d))")" = True ] \
|
|
&& ok "/api/v1/custom_emojis lists it" || ko "custom_emojis misses it: $(curl -s "$P/api/v1/custom_emojis" | head -c 300)"
|
|
document=$(pfetch -H 'Accept: application/activity+json' "https://privapub.test/grimaces/$code")
|
|
[ "$(echo "$document" | j "print(d['type'] == 'Emoji' and d['id'] == '$emoji_uri' and d['name'] == ':$code:' and d['icon']['url'] == '$emoji_url' and d['icon']['mediaType'] == 'image/png' and bool(d.get('updated')))")" = True ] \
|
|
&& ok "/grimaces/$code serves the Emoji, unsigned too" || ko "the Emoji document: $(echo "$document" | head -c 300)"
|
|
[ "$(pfetch -o /dev/null -w '%{http_code}' "$emoji_url")" = 200 ] && ok "its picture is served" || ko "its picture is not served"
|
|
|
|
echo " follows"
|
|
alice_on_m=$(mcurl -H "$MH" "$M/api/v2/search?q=@emoji_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"
|
|
alice_on_g=$(gcurl -H "$GH" "$G/api/v2/search?q=@emoji_alice@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
gcurl -o /dev/null -X POST -H "$GH" "$G/api/v1/accounts/$alice_on_g/follow"
|
|
alice_on_mk=$(mk users/show '{"username":"emoji_alice","host":"privapub.test"}' | j "print(d['id'])")
|
|
mk following/create "{\"userId\":\"$alice_on_mk\"}" >/dev/null
|
|
alice_on_a=$(acurl -H "$KH" "$AK/api/v2/search?q=@emoji_alice@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/accounts/$alice_on_a/follow"
|
|
alice_id=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
|
until_true 30 '[ "$(curl -s "$P/api/v1/accounts/$alice_id" | j "print(d[\"followers_count\"])")" -ge 4 ]' \
|
|
&& ok "mastouser, gtsuser, mkuser and akuser follow emoji_alice" || ko "not every peer follows emoji_alice"
|
|
mk_on_p=$(curl -s -H "$AH" "$P/api/v2/search?q=mkuser@misskey.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
ak_on_p=$(curl -s -H "$AH" "$P/api/v2/search?q=akuser@akkoma.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$mk_on_p/follow"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$ak_on_p/follow"
|
|
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/accounts/relationships?id[]=$mk_on_p&id[]=$ak_on_p" | j "print(all(r[\"following\"] for r in d))")" = True ]' \
|
|
&& ok "emoji_alice follows mkuser and akuser" || ko "emoji_alice's follows were not accepted"
|
|
|
|
echo " a post using it"
|
|
post=$(curl -s -X POST -H "$AH" "$P/api/v1/statuses" --data-urlencode "status=Hello :$code: from PrivaPub $run" -d visibility=public)
|
|
post_id=$(echo "$post" | j "print(d['id'])"); post_uri=$(echo "$post" | j "print(d['uri'])")
|
|
[ "$(echo "$post" | j "print(json.dumps(d.get('emojis')))" | has_code "$code")" = True ] && ok "the status lists :$code: among its emojis here" || ko "the status has no emoji here: $(echo "$post" | head -c 300)"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/$alice_on_m/statuses?limit=20" | j "s=next((s for s in d if s[\"uri\"]==\"$post_uri\"), None); print(json.dumps(s[\"emojis\"]) if s else \"null\")" | has_code "$code")" = True ]' \
|
|
&& ok "Mastodon shows the post with :$code:" || ko "Mastodon's copy has no emoji: $(mcurl -H "$MH" "$M/api/v1/accounts/$alice_on_m/statuses?limit=5" | head -c 300)"
|
|
until_true 30 '[ "$(on_gts "$post_uri" | j "print(json.dumps(d[\"statuses\"][0][\"emojis\"]) if d[\"statuses\"] else \"null\")" | has_code "$code")" = True ]' \
|
|
&& ok "GoToSocial shows the post with :$code:" || ko "GoToSocial's copy has no emoji: $(on_gts "$post_uri" | head -c 300)"
|
|
until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$post_uri" | j "print(json.dumps((d or {}).get(\"emojis\")))" | has_code "$code")" = True ]' \
|
|
&& ok "Misskey shows the note with :$code:" || ko "Misskey's copy has no emoji: $(mk_note_by_uri "$alice_on_mk" "$post_uri" | head -c 300)"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/accounts/$alice_on_a/statuses?limit=20" | j "s=next((s for s in d if s[\"uri\"]==\"$post_uri\"), None); print(json.dumps(s[\"emojis\"]) if s else \"null\")" | has_code "$code")" = True ]' \
|
|
&& ok "Akkoma shows the post with :$code:" || ko "Akkoma's copy has no emoji"
|
|
|
|
echo " a display name using it"
|
|
curl -s -o /dev/null -X PATCH -H "$AH" "$P/api/v1/accounts/update_credentials" --data-urlencode "display_name=Emoji Alice :$code:"
|
|
[ "$(curl -s "$P/api/v1/accounts/$alice_id" | j "print(json.dumps(d['emojis']))" | has_code "$code")" = True ] \
|
|
&& ok "emoji_alice's account lists :$code: here" || ko "the account here has no emoji"
|
|
until_true 45 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/$alice_on_m" | j "print(json.dumps(d[\"emojis\"]) if \"Emoji Alice\" in d[\"display_name\"] else \"null\")" | has_code "$code")" = True ]' \
|
|
&& ok "Mastodon shows the display name with :$code:" || ko "Mastodon's account: $(mcurl -H "$MH" "$M/api/v1/accounts/$alice_on_m" | j "print(d['display_name'], d['emojis'])")"
|
|
|
|
echo " reactions with it"
|
|
mk_post=$(mk notes/create "{\"text\":\"react to this, PrivaPub $run\",\"visibility\":\"public\"}" | j "print(d['createdNote']['id'])")
|
|
a_post=$(acurl -X POST -H "$KH" "$AK/api/v1/statuses" --data-urlencode "status=react to this too, PrivaPub $run" -d visibility=public | j "print(d['id'])")
|
|
until_true 30 '[ -n "$(p_home_id "react to this, PrivaPub $run")" ] && [ -n "$(p_home_id "react to this too, PrivaPub $run")" ]' \
|
|
&& ok "mkuser's and akuser's posts reach emoji_alice's home" || ko "the posts to react to never arrived"
|
|
mk_on_home=$(p_home_id "react to this, PrivaPub $run"); a_on_home=$(p_home_id "react to this too, PrivaPub $run")
|
|
[ "$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H "$AH" "$P/api/v1/pleroma/statuses/$mk_on_home/reactions/%3A$code%3A")" = 200 ] \
|
|
&& [ "$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H "$AH" "$P/api/v1/pleroma/statuses/$a_on_home/reactions/%3A$code%3A")" = 200 ] \
|
|
&& ok "emoji_alice reacts with :$code: to both" || ko "the reaction with the local emoji was refused"
|
|
until_true 30 '[ "$(mk notes/show "{\"noteId\":\"$mk_post\"}" | j "print(json.dumps(list((d.get(\"reactions\") or {}).keys())))" | has_code "$code")" = True ]' \
|
|
&& ok "the EmojiReact reaches Misskey as :$code:" || ko "Misskey's reactions: $(mk notes/show "{\"noteId\":\"$mk_post\"}" | j "print(d.get('reactions'), d.get('reactionEmojis'))")"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post" | j "print(json.dumps((d.get(\"pleroma\") or {}).get(\"emoji_reactions\") or d.get(\"emoji_reactions\") or []))" | has_code "$code")" = True ]' \
|
|
&& ok "the EmojiReact reaches Akkoma as :$code:" || ko "Akkoma's reactions: $(acurl -H "$KH" "$AK/api/v1/statuses/$a_post" | j "print((d.get('pleroma') or {}).get('emoji_reactions'))")"
|
|
|
|
echo " deleted"
|
|
[ "$(code_of "$ADMIN" POST "/clientapi/admin/emoji/delete?shortcode=$code")" = 200 ] && ok "the administrator deletes it" || ko "the emoji was not deleted"
|
|
[ "$(curl -s "$P/api/v1/custom_emojis" | j "print(any(e['shortcode'] == '$code' for e in d))")" = False ] && ok "custom_emojis no longer lists it" || ko "custom_emojis still lists it"
|
|
[ "$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "https://privapub.test/grimaces/$code")" = 410 ] \
|
|
&& ok "/grimaces/$code answers 410" || ko "/grimaces/$code answers $(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "https://privapub.test/grimaces/$code")"
|
|
[ "$(curl -s -X POST -H "$AH" "$P/api/v1/statuses" --data-urlencode "status=No more :$code: $run" -d visibility=public | has_code "$code")" = False ] \
|
|
&& [ "$(curl -s "$P/api/v1/statuses/$post_id" | j "print(json.dumps(d['emojis']))" | has_code "$code")" = False ] \
|
|
&& ok "posts here name it no more" || ko "a post still carries the deleted emoji"
|
|
log=$(curl -s -H "Authorization: Bearer $ADMIN" "$P/clientapi/moderator/log?limit=100")
|
|
[ "$(echo "$log" | j "print(all(any(i['action'] == a and i['targetKind'] == 'emoji' and i.get('targetLabel') == ':$code:' for i in d['items']) for a in ('emoji.insert', 'emoji.delete')))")" = True ] \
|
|
&& ok "the staff's log keeps the insert and the delete" || ko "the staff's log misses the emoji's acts"
|
|
curl -s -o /dev/null -X PATCH -H "$AH" "$P/api/v1/accounts/update_credentials" --data-urlencode 'display_name=emoji_alice'
|