544a170ead2c | atagen | 2026-10-20 11:47 UTC | 1 | # macltok request, custom emote and sticker packs (MSC2545) |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 2 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 3 | ## Problem |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 4 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 5 | Malice is adding an emoji picker for reactions that should also offer the user's custom emotes and stickers. Those live in MSC2545 image packs, spread over three places. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 6 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 7 | 1. `im.ponies.user_emotes` global account data, the user's personal pack |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 8 | 2. `im.ponies.room_emotes` room state, one pack per state key, in the current room |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 9 | 3. `im.ponies.emote_rooms` global account data, which names other rooms (and state keys) whose packs the user enabled everywhere |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 10 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 11 | Merging those, resolving pack vs image `usage`, and following `emote_rooms` into rooms the client may not have state for is protocol work every consumer would repeat, so it belongs in macltok. Malice can't do it today anyway. `global_account_data` covers 1 and 3, but there is no room state getter for 2. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 12 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 13 | ruma-events 0.34 already has the types behind `unstable-msc2545` (`image_pack.rs`, `RoomImagePackEventContent`, `AccountImagePackEventContent`, `ImagePackRoomsEventContent`, `PackUsage`), including the stable `m.image_pack` aliases. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 14 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 15 | ## Request |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 16 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 17 | A normalised query for the packs usable in a given room. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 18 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 19 | ```rust |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 20 | impl ClientHandle { |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 21 | /// user pack, then the room's own packs, then emote_rooms packs, deduplicated |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 22 | pub async fn image_packs(&self, room_id: Option<OwnedRoomId>) -> Result<Vec<ImagePack>, Error>; |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 23 | } |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 24 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 25 | pub struct ImagePack { |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 26 | pub id: String, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 27 | pub display_name: String, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 28 | pub avatar_url: Option<OwnedMxcUri>, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 29 | pub source: ImagePackSource, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 30 | pub images: Vec<PackImage>, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 31 | } |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 32 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 33 | pub enum ImagePackSource { |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 34 | User, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 35 | Room { room_id: OwnedRoomId, state_key: String }, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 36 | } |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 37 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 38 | pub struct PackImage { |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 39 | pub shortcode: String, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 40 | pub url: OwnedMxcUri, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 41 | pub body: Option<String>, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 42 | /// image usage if set, else the pack's, else both |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 43 | pub usage: Vec<PackUsage>, |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 44 | } |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 45 | ``` |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 46 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 47 | `id` only needs to be stable for a pack across calls, e.g. `user` or `room:<room_id>:<state_key>`. Serialised field names should match the rest of macltok's normalised types. Malice's web client already has the matching TypeScript shape in `web/src/lib/types.ts` (`ImagePack`, `PackImage`). |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 48 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 49 | It would also help to get a change event when any of the three sources updates, so the picker doesn't go stale, either a dedicated event or folding it into the existing room/account mirror. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 50 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 51 | ## Related, reactions with custom emotes (MSC4027) |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 52 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 53 | A custom emote reaction uses the image's mxc URI as the `m.annotation` key, with the shortcode in `com.beeper.reaction.shortcode` so clients without the pack can show a label. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 54 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 55 | - `toggle_reaction` only takes a key. An optional shortcode would let malice send the label. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 56 | - `normalise::timeline::reactions` drops the shortcode on receipt. `Reaction` could carry `shortcode: Option<String>`, taken from any sender's annotation for that key. |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 57 | |
544a170ead2c | atagen | 2026-10-20 11:47 UTC | 58 | Malice relaxed its own 32-char key limit for `mxc://` keys, so sending already works without the shortcode. |