Files
SocialPub/tools/pasture/scenarios/emoji.sh
T
thepraandClaude Opus 5.5 048eb15bb7 R7's emoji id is /grimaces/{shortcode}, the owner's name
The federated id of a local custom emoji (LocalEmojis.Path) was the plan's /emojis/{shortcode} awaiting the owner; it is
/grimaces/{shortcode}, in the style of the other routes, and freezes once it deploys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-18 10:29:53 +02:00

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') 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" | 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'