From e0cd3cec3fc73ab3a8a29686d29908a8820aebfb Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Tue, 11 Aug 2026 17:50:42 -0400 Subject: [PATCH] bug: add native emoji fallback for emoji routes back --- README.md | 6 ++++-- src/__tests__/handlers.test.ts | 27 +++++++++++++++++++++++++++ src/handlers/index.ts | 31 +++++++++++++++++++++++++++---- utils/emojiHelper.ts | 8 ++++++-- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f03e966..e15e61d 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,9 @@ I had some performance issues where the latency would spike to 1000ms+ every few There were a few interesting hurdles that made this a bit more confusing though. The first was that slack returns the `emoji.list` endpoint with not just regular emojis but also aliased emojis. The aliased emojis doesn't seem that hard at first untill you realize that someone could alias stock slack emojis. That means that we don't have a url to the image and to make it worse slack doesn't have an offically documented way to get the full list of stock emojis. Thankfully an amazing user ([@impressiver](https://github.com/impressiver)) put this all into a handy [gist](https://gist.github.com/impressiver/87b5b9682d935efba8936898fbfe1919) for everyone to use! It was last updated on 2020-12-22 so it's a bit out of date but slack doesn't seem to be changing their emojis too often so it should be fine for now. > [!NOTE] -> Turns out that to get the update emoji data all you need to do is open the react dev tools to the component tab and copy the consolidatedEmojis field. Just make sure this is done in a brand new workspace so there are no existing emojis that get lumped in. +> The map now regenerates itself. Run `bun run scripts/build-emoji-map.ts [version]` and it rebuilds [`utils/slack_emoji_map.json`](./utils/slack_emoji_map.json) from [iamcal/emoji-data](https://github.com/iamcal/emoji-data), which is the dataset Slack's own emoji picker is built on. The dataset version tracks the cdn asset version, so `emoji-datasource@16.0.0` lines up with `production-standard-emoji-assets/16.0/`. Bump both together in the script and in [`utils/emojiHelper.ts`](./utils/emojiHelper.ts). +> +> The old manual route, for posterity: open the react dev tools to the component tab and copy the `consolidatedEmojis` field, in a brand new workspace so no custom emojis get lumped in. ```json { @@ -92,7 +94,7 @@ There were a few interesting hurdles that made this a bit more confusing though. } { - "grinning": "https://a.slack-edge.com/production-standard-emoji-assets/14.0/google-medium/1f601.png" + "grinning": "https://a.slack-edge.com/production-standard-emoji-assets/16.0/google-xlarge/1f601.png" } ``` diff --git a/src/__tests__/handlers.test.ts b/src/__tests__/handlers.test.ts index 2279430..41a7d1c 100644 --- a/src/__tests__/handlers.test.ts +++ b/src/__tests__/handlers.test.ts @@ -147,6 +147,33 @@ describe("handlers", () => { expect(response.status).toBe(404); }); + + it("returns native emoji when not cached", async () => { + const cache = createMockCache(); + const handlers = createHandlers(cache); + const request = new Request("http://localhost/emojis/sparkling_heart"); + const response = await handlers.handleGetEmoji(request, noopAnalytics); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.name).toBe("sparkling_heart"); + expect(body.imageUrl).toContain("/1f496.png"); + }); + }); + + describe("handleEmojiRedirect", () => { + it("redirects to native emoji when not cached", async () => { + const cache = createMockCache(); + const handlers = createHandlers(cache); + const request = new Request("http://localhost/emojis/bangbang/r"); + const response = await handlers.handleEmojiRedirect( + request, + noopAnalytics, + ); + + expect(response.status).toBe(302); + expect(response.headers.get("location")).toContain("/203c-fe0f.png"); + }); }); describe("handleListEmojis", () => { diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 71b48f2..c19edfd 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -6,6 +6,7 @@ import type { SlackCache } from "../cache"; import { config } from "../config"; import type { RouteHandlerWithAnalytics } from "../lib/analytics-wrapper"; import { lastSegment, pathSegment, queryParam } from "../lib/fast-url"; +import { getEmojiUrl } from "../../utils/emojiHelper"; /** * Parse a string to a positive integer, returning a fallback if invalid @@ -163,8 +164,21 @@ export function createHandlers(cache: SlackCache) { const emoji = await cache.getEmoji(emojiName); if (!emoji) { - recordAnalytics(404); - return Response.json({ message: "Emoji not found" }, { status: 404 }); + const nativeEmojiUrl = getEmojiUrl(emojiName); + if (!nativeEmojiUrl) { + recordAnalytics(404); + return Response.json({ message: "Emoji not found" }, { status: 404 }); + } + + recordAnalytics(200); + return Response.json({ + type: "emoji", + id: `native:${emojiName.toLowerCase()}`, + name: emojiName.toLowerCase(), + alias: null, + imageUrl: nativeEmojiUrl, + expiration: null, + }); } recordAnalytics(200); @@ -179,8 +193,17 @@ export function createHandlers(cache: SlackCache) { const emoji = await cache.getEmoji(emojiName); if (!emoji) { - recordAnalytics(404); - return Response.json({ message: "Emoji not found" }, { status: 404 }); + const nativeEmojiUrl = getEmojiUrl(emojiName); + if (!nativeEmojiUrl) { + recordAnalytics(404); + return Response.json({ message: "Emoji not found" }, { status: 404 }); + } + + recordAnalytics(302); + return new Response(null, { + status: 302, + headers: { Location: nativeEmojiUrl }, + }); } recordAnalytics(302); diff --git a/utils/emojiHelper.ts b/utils/emojiHelper.ts index 6b79ade..4e29926 100644 --- a/utils/emojiHelper.ts +++ b/utils/emojiHelper.ts @@ -43,14 +43,18 @@ class EmojiMap { const emojiMap = new EmojiMap(emojilib); +/** Slack bumps this when it ships a new emoji asset set */ +const ASSET_VERSION = "16.0"; +const ASSET_STYLE = "google-xlarge"; + /** * Gets the Slack CDN URL for an emoji * @param keyword The emoji name/shortcode * @returns The CDN URL, or null if emoji not found */ export function getEmojiUrl(keyword: string): string | null { - const codepoint = emojiMap.getCodepoint(keyword); + const codepoint = emojiMap.getCodepoint(keyword.toLowerCase()); if (!codepoint) return null; - return `https://a.slack-edge.com/production-standard-emoji-assets/14.0/google-medium/${codepoint.toLowerCase()}.png`; + return `https://a.slack-edge.com/production-standard-emoji-assets/${ASSET_VERSION}/${ASSET_STYLE}/${codepoint.toLowerCase()}.png`; } -- 2.51.2