diff --git a/CLAUDE.md b/CLAUDE.md index 1c7e09f..137dfee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 post's replies and thread | `/peasants/{name}/scribbles/{id}/replies`, `/peasants/{name}/scribbles/{id}/context` | | 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 -id (Mastodon's own path, with the shortcode for its number) and waits for the owner's confirmation before R7 deploys; it -freezes once it has federated, and every emoji id elsewhere names it. Agreed for later: `/drool` and `/echoes` (likes, shares). `/gossip` was +All of these have federated, so they are frozen. `/grimaces/{shortcode}` is a custom emoji's id, named by the owner +(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 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. @@ -108,7 +107,7 @@ PrivaPub/ ASP.NET Core Web API, net10.0 Federation/ Controllers/ PeasantsController (actor, outbox, followers, following, posts, inboxes), 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 (authoritative fetch, key verification, WebFinger), ActorDocument (parser) 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 emoji changes (Mastodon sends none either). - **The `Emoji` sent** (`LocalEmojis.Tag`, Mastodon's shape; GoToSocial takes none without its `id`): - `{"id": "{base}/emojis/{shortcode}", "type": "Emoji", "name": ":shortcode:", "updated": , + `{"id": "{base}/grimaces/{shortcode}", "type": "Emoji", "name": ":shortcode:", "updated": , "icon": {"type": "Image", "mediaType": "image/png" | "image/gif", "url": }}`, and the same with the `@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 @@ -1245,7 +1244,7 @@ tools/pasture/run.sh down # removes e no address. - **Custom emoji (`scenarios/emoji.sh`, needs gts, mastodon, misskey and akkoma; after moderation in batch A):** the administrator adds `:pasture_:` (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 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. diff --git a/FEDERATION.md b/FEDERATION.md index d33b5ed..76883a2 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -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` | | Followers on the asking server (FEP-8fcf) | `/peasants/{name}/groupies/roll-call` | | 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}` | 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`: ```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", "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 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 - 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. - **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 diff --git a/PrivaPub.ClientModels/Admin/ViewLocalEmoji.cs b/PrivaPub.ClientModels/Admin/ViewLocalEmoji.cs index 058ee67..418c8f6 100644 --- a/PrivaPub.ClientModels/Admin/ViewLocalEmoji.cs +++ b/PrivaPub.ClientModels/Admin/ViewLocalEmoji.cs @@ -19,7 +19,7 @@ namespace PrivaPub.ClientModels.Admin public int? Width { get; set; } public int? Height { get; set; } 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 UpdatedAt { get; set; } public DateTime ImageUpdatedAt { get; set; } diff --git a/PrivaPub/Controllers/ClientToServer/EmojiAdminController.cs b/PrivaPub/Controllers/ClientToServer/EmojiAdminController.cs index c80c120..b8a8e0f 100644 --- a/PrivaPub/Controllers/ClientToServer/EmojiAdminController.cs +++ b/PrivaPub/Controllers/ClientToServer/EmojiAdminController.cs @@ -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 // 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, Route("clientapi/admin/emoji"), Authorize(Policy = Policies.IsAdmin)] diff --git a/PrivaPub/Domain/Content/LocalEmojis.cs b/PrivaPub/Domain/Content/LocalEmojis.cs index 78b0c3a..2acef14 100644 --- a/PrivaPub/Domain/Content/LocalEmojis.cs +++ b/PrivaPub/Domain/Content/LocalEmojis.cs @@ -21,7 +21,7 @@ namespace PrivaPub.Domain.Content { /// 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). - public const string Path = "emojis"; + public const string Path = "grimaces"; public const int MaxShortcodeLength = 64; public const int MaxPerObject = 64;//what ObjectShapes reads of anyone's object const string ServerUsePrefix = "emoji:"; diff --git a/PrivaPub/Federation/Controllers/EmojisController.cs b/PrivaPub/Federation/Controllers/EmojisController.cs index 7e9ad15..22428db 100644 --- a/PrivaPub/Federation/Controllers/EmojisController.cs +++ b/PrivaPub/Federation/Controllers/EmojisController.cs @@ -11,7 +11,7 @@ using System.Text.Json.Nodes; 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 // 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 diff --git a/PrivaPub/Models/Post/LocalEmoji.cs b/PrivaPub/Models/Post/LocalEmoji.cs index 1bdcdbe..b669fe8 100644 --- a/PrivaPub/Models/Post/LocalEmoji.cs +++ b/PrivaPub/Models/Post/LocalEmoji.cs @@ -4,7 +4,7 @@ namespace PrivaPub.Models.Post { // 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 - // 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:" 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. public class LocalEmoji : Entity diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 179344c..6494ed0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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; `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 - `EmojiReact`s, each served as an `Emoji` at its id `/emojis/{shortcode}`. Chosen while building it, for the owner to - confirm: **the id's path `/emojis/{shortcode}`** (the plan's name, 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 + `EmojiReact`s, each served as an `Emoji` at its id `/grimaces/{shortcode}`. The id's path **`/grimaces/{shortcode}`** is the + 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 (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 410; what posts and profiles render is the emoji as they are now, and no `Update` is sent when one changes; its id is diff --git a/tools/pasture/scenarios/emoji.sh b/tools/pasture/scenarios/emoji.sh index b336b05..716e121 100644 --- a/tools/pasture/scenarios/emoji.sh +++ b/tools/pasture/scenarios/emoji.sh @@ -1,5 +1,5 @@ # 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 # 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 @@ -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' \ -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/emojis/$code')")" = True ] \ - && ok "an administrator adds :$code: (a PNG, its id /emojis/$code)" || ko "the emoji was not added: $(echo "$added" | head -c 300)" +[ "$(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/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 ] \ - && 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" echo " follows" @@ -128,8 +128,8 @@ until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post" | j "print(json 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/emojis/$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")" +[ "$(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"