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
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-18 10:29:53 +02:00
1 parent 9b792042e6
commit 048eb15bb7
9 files changed
+23 -25

No files matched your search

+6 -7
View File
@@ -65,11 +65,10 @@ actor-scoped route gets a name in the same spirit, agreed with the owner, and a
| A group's members and moderators | `/peasants/{name}/flock`, `/peasants/{name}/wardens` | | A group's members and moderators | `/peasants/{name}/flock`, `/peasants/{name}/wardens` |
| A post's replies and thread | `/peasants/{name}/scribbles/{id}/replies`, `/peasants/{name}/scribbles/{id}/context` | | A post's replies and thread | `/peasants/{name}/scribbles/{id}/replies`, `/peasants/{name}/scribbles/{id}/context` |
| Token refresh | `/clientapi/user/sniff/again` | | Token refresh | `/clientapi/user/sniff/again` |
| A custom emoji (`Emoji`, R7) | `/emojis/{shortcode}` (`LocalEmojis.Path`, the one place it is written) | | A custom emoji (`Emoji`, R7) | `/grimaces/{shortcode}` (`LocalEmojis.Path`, the one place it is written) |
All of these have federated, so they are frozen, but `/emojis/{shortcode}`: it is the plan's name for a custom emoji's All of these have federated, so they are frozen. `/grimaces/{shortcode}` is a custom emoji's id, named by the owner
id (Mastodon's own path, with the shortcode for its number) and waits for the owner's confirmation before R7 deploys; it (2026-10-18) in the style of the others; it freezes once R7 deploys, and every emoji id elsewhere names it. Agreed for later: `/drool` and `/echoes` (likes, shares). `/gossip` was
freezes once it has federated, and every emoji id elsewhere names it. Agreed for later: `/drool` and `/echoes` (likes, shares). `/gossip` was
agreed for replies before they were served at `…/scribbles/{id}/replies` (2026-10-06); whether it is still wanted is agreed for replies before they were served at `…/scribbles/{id}/replies` (2026-10-06); whether it is still wanted is
asked in the completion program's R10. asked in the completion program's R10.
@@ -108,7 +107,7 @@ PrivaPub/ ASP.NET Core Web API, net10.0
Federation/ Federation/
Controllers/ PeasantsController (actor, outbox, followers, following, posts, inboxes), Controllers/ PeasantsController (actor, outbox, followers, following, posts, inboxes),
WellKnownController (webfinger, nodeinfo), UsersController (redirect), WellKnownController (webfinger, nodeinfo), UsersController (redirect),
EmojisController (a custom emoji's `Emoji` at /emojis/{shortcode}) EmojisController (a custom emoji's `Emoji` at /grimaces/{shortcode})
Actors/ LocalActorService (LocalActor, Keys, ReservedName), RemoteActorService Actors/ LocalActorService (LocalActor, Keys, ReservedName), RemoteActorService
(authoritative fetch, key verification, WebFinger), ActorDocument (parser) (authoritative fetch, key verification, WebFinger), ActorDocument (parser)
Objects/ Origin, ActivityJson, NoteParser, Addressing, ContentSanitizer Objects/ Origin, ActivityJson, NoteParser, Addressing, ContentSanitizer
@@ -536,7 +535,7 @@ client's role route never does.
a new picture shows (its `updated` moves, so servers holding the old one fetch it again). No `Update` is sent when an a new picture shows (its `updated` moves, so servers holding the old one fetch it again). No `Update` is sent when an
emoji changes (Mastodon sends none either). emoji changes (Mastodon sends none either).
- **The `Emoji` sent** (`LocalEmojis.Tag`, Mastodon's shape; GoToSocial takes none without its `id`): - **The `Emoji` sent** (`LocalEmojis.Tag`, Mastodon's shape; GoToSocial takes none without its `id`):
`{"id": "{base}/emojis/{shortcode}", "type": "Emoji", "name": ":shortcode:", "updated": <ImageUpdatedAt, ms>, `{"id": "{base}/grimaces/{shortcode}", "type": "Emoji", "name": ":shortcode:", "updated": <ImageUpdatedAt, ms>,
"icon": {"type": "Image", "mediaType": "image/png" | "image/gif", "url": <the picture>}}`, and the same with the "icon": {"type": "Image", "mediaType": "image/png" | "image/gif", "url": <the picture>}}`, and the same with the
`@context` at its id (`Federation/Controllers/EmojisController.cs`, `application/activity+json`, unsigned under `@context` at its id (`Federation/Controllers/EmojisController.cs`, `application/activity+json`, unsigned under
SecureMode too, since an emoji tells nothing about anyone; a disabled one 404, a deleted one 410 with a Tombstone; a SecureMode too, since an emoji tells nothing about anyone; a disabled one 404, a deleted one 410 with a Tombstone; a
@@ -1245,7 +1244,7 @@ tools/pasture/run.sh down # removes e
no address. no address.
- **Custom emoji (`scenarios/emoji.sh`, needs gts, mastodon, misskey and akkoma; after moderation in batch A):** the - **Custom emoji (`scenarios/emoji.sh`, needs gts, mastodon, misskey and akkoma; after moderation in batch A):** the
administrator adds `:pasture_<run>:` (409 for it again, a persona refused the list); `/api/v1/custom_emojis` lists it administrator adds `:pasture_<run>:` (409 for it again, a persona refused the list); `/api/v1/custom_emojis` lists it
and `/emojis/{shortcode}` serves the `Emoji` unsigned; the four peers follow emoji_alice, whose post using it shows it and `/grimaces/{shortcode}` serves the `Emoji` unsigned; the four peers follow emoji_alice, whose post using it shows it
in the status's `emojis` on Mastodon, GoToSocial (by URI), Misskey (the note's `emojis`) and Akkoma; her display name in the status's `emojis` on Mastodon, GoToSocial (by URI), Misskey (the note's `emojis`) and Akkoma; her display name
shows it on Mastodon; her `EmojiReact` with it reaches Misskey and Akkoma as that emoji; deleted, it leaves the list, shows it on Mastodon; her `EmojiReact` with it reaches Misskey and Akkoma as that emoji; deleted, it leaves the list,
its id answers 410 and posts here carry it no more; the staff's log keeps the insert and the delete. its id answers 410 and posts here carry it no more; the staff's log keeps the insert and the delete.
+3 -3
View File
@@ -133,7 +133,7 @@ actor document, a collection, NodeInfo or a delivery relates two avatars of the
| A persona's wall (FEP-400e) | `/peasants/{name}/graffiti` | | A persona's wall (FEP-400e) | `/peasants/{name}/graffiti` |
| Followers on the asking server (FEP-8fcf) | `/peasants/{name}/groupies/roll-call` | | Followers on the asking server (FEP-8fcf) | `/peasants/{name}/groupies/roll-call` |
| Featured (pinned) posts, featured hashtags | `/peasants/{name}/trophies`, `/peasants/{name}/tattoos` | | Featured (pinned) posts, featured hashtags | `/peasants/{name}/trophies`, `/peasants/{name}/tattoos` |
| A custom emoji of this server (`Emoji`) | `/emojis/{shortcode}` | | A custom emoji of this server (`Emoji`) | `/grimaces/{shortcode}` |
| Profile and post pages | `/@{name}`, `/@{name}/{id}` | | Profile and post pages | `/@{name}`, `/@{name}/{id}` |
The names are the project's own and are stable; resolve actors through WebFinger, not by guessing a path. The names are the project's own and are stable; resolve actors through WebFinger, not by guessing a path.
@@ -367,7 +367,7 @@ Mastodon sends them, on a post or poll whose words, title, warning or options na
whose name, bio or fields do, and on an `EmojiReact`: whose name, bio or fields do, and on an `EmojiReact`:
```json ```json
{"id": "https://privapub.thepra.dev/emojis/blobcat", "type": "Emoji", "name": ":blobcat:", {"id": "https://privapub.thepra.dev/grimaces/blobcat", "type": "Emoji", "name": ":blobcat:",
"updated": "2026-10-17T12:00:00.000Z", "updated": "2026-10-17T12:00:00.000Z",
"icon": {"type": "Image", "mediaType": "image/png", "url": "https://privapub.thepra.dev/media/files/2026/10/….png"}} "icon": {"type": "Image", "mediaType": "image/png", "url": "https://privapub.thepra.dev/media/files/2026/10/….png"}}
``` ```
@@ -500,7 +500,7 @@ Posts with a location (shown to nearby users of this server) never leave the ser
- **Reading our documents (SecureMode).** privapub.thepra.dev answers ActivityPub GETs only when they are signed, like - **Reading our documents (SecureMode).** privapub.thepra.dev answers ActivityPub GETs only when they are signed, like
Mastodon's authorized fetch; the server's own actors (the instance actor `/peasants/privapub`, the reporter Mastodon's authorized fetch; the server's own actors (the instance actor `/peasants/privapub`, the reporter
`/peasants/privapub_reports` and the reader of feeds `/peasants/privapub_feeds`) are the exception, since their keys `/peasants/privapub_reports` and the reader of feeds `/peasants/privapub_feeds`) are the exception, since their keys
are needed first, and so are its custom emoji at `/emojis/{shortcode}`. are needed first, and so are its custom emoji at `/grimaces/{shortcode}`.
A browser asking for HTML is redirected to the public page instead. A browser asking for HTML is redirected to the public page instead.
- **Posts that are not public** (followers-only, direct, circle) are served to a signed request from someone they were - **Posts that are not public** (followers-only, direct, circle) are served to a signed request from someone they were
for, or from the instance actor of a server where someone they were for lives, and to nobody else (404). Once deleted for, or from the instance actor of a server where someone they were for lives, and to nobody else (404). Once deleted
@@ -19,7 +19,7 @@ namespace PrivaPub.ClientModels.Admin
public int? Width { get; set; } public int? Width { get; set; }
public int? Height { get; set; } public int? Height { get; set; }
public long Size { get; set; } = 0; public long Size { get; set; } = 0;
public string Uri { get; set; }//its id on other servers (/emojis/{shortcode}): why a shortcode never changes public string Uri { get; set; }//its id on other servers (/grimaces/{shortcode}): why a shortcode never changes
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; } public DateTime UpdatedAt { get; set; }
public DateTime ImageUpdatedAt { get; set; } public DateTime ImageUpdatedAt { get; set; }
@@ -22,7 +22,7 @@ namespace PrivaPub.Controllers.ClientToServer
{ {
// The server's custom emoji from the administrator's page (completion program R7): listed, added, changed and deleted // The server's custom emoji from the administrator's page (completion program R7): listed, added, changed and deleted
// by an administrator, checked again against the database. Every act is kept in the staff's log (emoji.insert, // by an administrator, checked again against the database. Every act is kept in the staff's log (emoji.insert,
// emoji.update, emoji.delete). An emoji is never renamed: its shortcode is its id on other servers (/emojis/{shortcode}). // emoji.update, emoji.delete). An emoji is never renamed: its shortcode is its id on other servers (/grimaces/{shortcode}).
[ApiController, [ApiController,
Route("clientapi/admin/emoji"), Route("clientapi/admin/emoji"),
Authorize(Policy = Policies.IsAdmin)] Authorize(Policy = Policies.IsAdmin)]
+1 -1
View File
@@ -21,7 +21,7 @@ namespace PrivaPub.Domain.Content
{ {
/// <summary>Where each emoji is served as an ActivityPub `Emoji` object: /emojis/{shortcode}, its id on other servers. /// <summary>Where each emoji is served as an ActivityPub `Emoji` object: /emojis/{shortcode}, its id on other servers.
/// The plan's name, waiting for the owner's confirmation; it is frozen once it has federated (CLAUDE.md, routes).</summary> /// The plan's name, waiting for the owner's confirmation; it is frozen once it has federated (CLAUDE.md, routes).</summary>
public const string Path = "emojis"; public const string Path = "grimaces";
public const int MaxShortcodeLength = 64; public const int MaxShortcodeLength = 64;
public const int MaxPerObject = 64;//what ObjectShapes reads of anyone's object public const int MaxPerObject = 64;//what ObjectShapes reads of anyone's object
const string ServerUsePrefix = "emoji:"; const string ServerUsePrefix = "emoji:";
@@ -11,7 +11,7 @@ using System.Text.Json.Nodes;
namespace PrivaPub.Federation.Controllers namespace PrivaPub.Federation.Controllers
{ {
// Each of the server's custom emoji as an ActivityPub `Emoji` object at its id, /emojis/{shortcode} (completion program // Each of the server's custom emoji as an ActivityPub `Emoji` object at its id, /grimaces/{shortcode} (completion program
// R7; the path is LocalEmojis.Path): the tag that posts, actors and reactions carry, with its @context. GoToSocial takes // R7; the path is LocalEmojis.Path): the tag that posts, actors and reactions carry, with its @context. GoToSocial takes
// an emoji only with an id, and may read it there. A disabled emoji is not served (404, it may come back); a deleted one // an emoji only with an id, and may read it there. A disabled emoji is not served (404, it may come back); a deleted one
// answers 410 with a Tombstone. An emoji tells nothing about anyone, so it is served unsigned under SecureMode too, as the // answers 410 with a Tombstone. An emoji tells nothing about anyone, so it is served unsigned under SecureMode too, as the
+1 -1
View File
@@ -4,7 +4,7 @@ namespace PrivaPub.Models.Post
{ {
// One of this server's own custom emoji (completion program R7), added by an administrator // One of this server's own custom emoji (completion program R7), added by an administrator
// (/clientapi/admin/emoji, EmojiAdminController). Its shortcode is its name everywhere: `:shortcode:` in a post, and // (/clientapi/admin/emoji, EmojiAdminController). Its shortcode is its name everywhere: `:shortcode:` in a post, and
// its federated id (/emojis/{shortcode}, Domain/Content/LocalEmojis.Path), which is why it is never renamed. Its picture // its federated id (/grimaces/{shortcode}, Domain/Content/LocalEmojis.Path), which is why it is never renamed. Its picture
// is a MediaAttachment nobody owns, held by ServerUse "emoji:<shortcode>" while the emoji exists. Deleted, the row stays // is a MediaAttachment nobody owns, held by ServerUse "emoji:<shortcode>" while the emoji exists. Deleted, the row stays
// (DeletedAt) so its id answers 410; adding the same shortcode again brings the row back with a new picture. // (DeletedAt) so its id answers 410; adding the same shortcode again brings the row back with a new picture.
public class LocalEmoji : Entity public class LocalEmoji : Entity
+2 -3
View File
@@ -130,9 +130,8 @@ Written 2026-10-01 from the original 2023 code, the decePubClient UI, a federati
(`LocalEmoji`), added, changed and deleted by an administrator on `/clientapi/admin/emoji` and kept in the staff's log; (`LocalEmoji`), added, changed and deleted by an administrator on `/clientapi/admin/emoji` and kept in the staff's log;
`GET /api/v1/custom_emojis`; taken into local posts (words, title, warning, poll options), profiles (name, bio, fields) `GET /api/v1/custom_emojis`; taken into local posts (words, title, warning, poll options), profiles (name, bio, fields)
and reactions (Pleroma's reactions and announcements), sent as Mastodon's `Emoji` tags on Notes, Questions, actors and and reactions (Pleroma's reactions and announcements), sent as Mastodon's `Emoji` tags on Notes, Questions, actors and
`EmojiReact`s, each served as an `Emoji` at its id `/emojis/{shortcode}`. Chosen while building it, for the owner to `EmojiReact`s, each served as an `Emoji` at its id `/grimaces/{shortcode}`. The id's path **`/grimaces/{shortcode}`** is the
confirm: **the id's path `/emojis/{shortcode}`** (the plan's name, defined once as `LocalEmojis.Path`; it freezes once owner's (2026-10-18; defined once as `LocalEmojis.Path`; it freezes once federated); an emoji is never renamed (its shortcode is its id); its picture is a PNG within 256 px, or an animated GIF
federated); an emoji is never renamed (its shortcode is its id); its picture is a PNG within 256 px, or an animated GIF
(an animated WebP becomes one too), made smaller until it fits GoToSocial's 100 KiB; `custom_emojis` lists only those (an animated WebP becomes one too), made smaller until it fits GoToSocial's 100 KiB; `custom_emojis` lists only those
in the picker, as Mastodon does; a disabled emoji is kept but offered and rendered nowhere (its id 404), a deleted one in the picker, as Mastodon does; a disabled emoji is kept but offered and rendered nowhere (its id 404), a deleted one
410; what posts and profiles render is the emoji as they are now, and no `Update` is sent when one changes; its id is 410; what posts and profiles render is the emoji as they are now, and no `Update` is sent when one changes; its id is
+7 -7
View File
@@ -1,5 +1,5 @@
# Custom emoji (completion program R7). The administrator adds one of the server's own emoji through # 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 /emojis/{shortcode} serves as an ActivityPub Emoji (signed or # /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 # 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 # 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 # 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
@@ -60,16 +60,16 @@ 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' \ 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") -F "image=@$work/emoji.png;type=image/png")
emoji_url=$(echo "$added" | j "print(d['url'])"); emoji_uri=$(echo "$added" | j "print(d['uri'])") 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/emojis/$code')")" = True ] \ [ "$(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 /emojis/$code)" || ko "the emoji was not added: $(echo "$added" | head -c 300)" && 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 ] \ [ "$(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" && 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" [ "$(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 ] \ [ "$(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)" && 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/emojis/$code") 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 ] \ [ "$(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 "/emojis/$code serves the Emoji, unsigned too" || ko "the Emoji document: $(echo "$document" | head -c 300)" && 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" [ "$(pfetch -o /dev/null -w '%{http_code}' "$emoji_url")" = 200 ] && ok "its picture is served" || ko "its picture is not served"
echo " follows" echo " follows"
@@ -128,8 +128,8 @@ until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post" | j "print(json
echo " deleted" echo " deleted"
[ "$(code_of "$ADMIN" POST "/clientapi/admin/emoji/delete?shortcode=$code")" = 200 ] && ok "the administrator deletes it" || ko "the emoji was not 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" [ "$(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/emojis/$code")" = 410 ] \ [ "$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "https://privapub.test/grimaces/$code")" = 410 ] \
&& ok "/emojis/$code answers 410" || ko "/emojis/$code answers $(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "https://privapub.test/emojis/$code")" && 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 -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 ] \ && [ "$(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" && ok "posts here name it no more" || ko "a post still carries the deleted emoji"