diff --git a/package.json b/package.json index 77211d865..ed88652cd 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,7 @@ "long": "5.3.2", "markdown-it": "^14.1.0", "node-edge-tts": "^1.2.9", + "node-emoji": "^2.2.0", "osc-progress": "^0.3.0", "pdfjs-dist": "^5.4.530", "playwright-core": "1.58.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95b940c97..c019b62d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: node-edge-tts: specifier: ^1.2.9 version: 1.2.9 + node-emoji: + specifier: ^2.2.0 + version: 2.2.0 osc-progress: specifier: ^0.3.0 version: 0.3.0 @@ -2382,6 +2385,10 @@ packages: '@sinclair/typebox@0.34.47': resolution: {integrity: sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@slack/bolt@4.6.0': resolution: {integrity: sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ==} engines: {node: '>=18', npm: '>=8.6.0'} @@ -3190,6 +3197,10 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + chmodrp@1.0.2: resolution: {integrity: sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==} @@ -3459,6 +3470,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -4419,6 +4433,10 @@ packages: resolution: {integrity: sha512-fvfW1dUgJdZAdTniC6MzLTMwnNUFKGKaUdRJ1OsveOYlfnPUETBU973CG89565txvbBowCQ4Czdeu3qSX8bNOg==} hasBin: true + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -5043,6 +5061,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + sleep-promise@9.1.0: resolution: {integrity: sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==} @@ -5194,6 +5216,7 @@ packages: tar@7.5.4: resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} @@ -5323,6 +5346,10 @@ packages: resolution: {integrity: sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==} engines: {node: '>=20.18.1'} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unicode-properties@1.4.1: resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} @@ -7918,6 +7945,8 @@ snapshots: '@sinclair/typebox@0.34.47': {} + '@sindresorhus/is@4.6.0': {} + '@slack/bolt@4.6.0(@types/express@5.0.6)': dependencies: '@slack/logger': 4.0.0 @@ -9063,6 +9092,8 @@ snapshots: chalk@5.6.2: {} + char-regex@1.0.2: {} + chmodrp@1.0.2: optional: true @@ -9328,6 +9359,8 @@ snapshots: emoji-regex@9.2.2: {} + emojilib@2.4.0: {} + encodeurl@2.0.0: {} entities@4.5.0: {} @@ -10404,6 +10437,13 @@ snapshots: - supports-color - utf-8-validate + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -11245,6 +11285,10 @@ snapshots: sisteransi@1.0.5: {} + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + sleep-promise@9.1.0: optional: true @@ -11506,6 +11550,8 @@ snapshots: undici@7.19.0: {} + unicode-emoji-modifier-base@1.0.0: {} + unicode-properties@1.4.1: dependencies: base64-js: 1.5.1 diff --git a/src/slack/actions.ts b/src/slack/actions.ts index 4ce6b37ca..c4525674c 100644 --- a/src/slack/actions.ts +++ b/src/slack/actions.ts @@ -1,4 +1,5 @@ import type { WebClient } from "@slack/web-api"; +import { which as emojiWhich } from "node-emoji"; import { loadConfig } from "../config/config.js"; import { logVerbose } from "../globals.js"; @@ -47,11 +48,24 @@ function resolveToken(explicit?: string, accountId?: string) { return token; } +/** + * Normalize emoji for Slack reactions. + * Converts Unicode emojis to Slack shortcodes (👀 → eyes). + * Strips colons from already-formatted shortcodes (:eyes: → eyes). + */ function normalizeEmoji(raw: string) { const trimmed = raw.trim(); if (!trimmed) { throw new Error("Emoji is required for Slack reactions"); } + + // Try Unicode → shortcode conversion first + const shortcode = emojiWhich(trimmed); + if (shortcode) { + return shortcode; + } + + // Fall back to stripping colons (for already-formatted shortcodes) return trimmed.replace(/^:+|:+$/g, ""); } diff --git a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts index c4b4b96ed..b8e4bb168 100644 --- a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts +++ b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts @@ -107,7 +107,7 @@ describe("monitorSlackProvider tool results", () => { expect(reactMock).toHaveBeenCalledWith({ channel: "C1", timestamp: "456", - name: "👀", + name: "eyes", }); });