@crewhaus/channel-adapter-telegram 0.1.3 โ 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.test.ts +76 -0
- package/src/index.ts +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/channel-adapter-telegram",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Telegram channel adapter: secret_token webhook verification, message/edited_message/callback_query parsing, group + topic session keying (Section 33)",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test": "bun test src"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@crewhaus/errors": "0.1.
|
|
15
|
+
"@crewhaus/errors": "0.1.4"
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"author": {
|
package/src/index.test.ts
CHANGED
|
@@ -568,6 +568,82 @@ describe("sendReply / setTyping (T3)", () => {
|
|
|
568
568
|
});
|
|
569
569
|
});
|
|
570
570
|
|
|
571
|
+
describe("react (Phase 3 ยง3.2)", () => {
|
|
572
|
+
function captureFetch() {
|
|
573
|
+
const calls: Array<{ url: string; init: RequestInit }> = [];
|
|
574
|
+
const f = (async (input: string | Request | URL, init?: RequestInit) => {
|
|
575
|
+
calls.push({ url: String(input), init: init ?? {} });
|
|
576
|
+
return new Response(JSON.stringify({ ok: true, result: true }), {
|
|
577
|
+
status: 200,
|
|
578
|
+
headers: { "content-type": "application/json" },
|
|
579
|
+
});
|
|
580
|
+
}) as unknown as typeof fetch;
|
|
581
|
+
return { calls, fetch: f };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const event: InboundEvent = {
|
|
585
|
+
idempotencyKey: "100001",
|
|
586
|
+
workspaceId: "4242",
|
|
587
|
+
channelId: "4242",
|
|
588
|
+
userId: "4242",
|
|
589
|
+
ts: "7",
|
|
590
|
+
text: "hello",
|
|
591
|
+
subtype: "message",
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
test("posts setMessageReaction with chat_id, message_id, and a mapped allowed emoji", async () => {
|
|
595
|
+
const { calls, fetch: f } = captureFetch();
|
|
596
|
+
const a = createTelegramAdapter(
|
|
597
|
+
{ botToken: BOT_TOKEN, secretToken: SECRET },
|
|
598
|
+
{ apiBaseUrl: "https://test.telegram.local", fetch: f },
|
|
599
|
+
);
|
|
600
|
+
expect(a.react).toBeDefined();
|
|
601
|
+
await a.react?.({ event, emoji: "eyes" });
|
|
602
|
+
expect(calls.length).toBe(1);
|
|
603
|
+
expect(calls[0]?.url).toBe(`https://test.telegram.local/bot${BOT_TOKEN}/setMessageReaction`);
|
|
604
|
+
const body = JSON.parse(String(calls[0]?.init.body));
|
|
605
|
+
expect(body.chat_id).toBe(4242);
|
|
606
|
+
expect(body.message_id).toBe(7);
|
|
607
|
+
expect(body.reaction).toEqual([{ type: "emoji", emoji: "๐" }]);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("maps white_check_mark/warning to Telegram-allowed emoji", async () => {
|
|
611
|
+
const { calls, fetch: f } = captureFetch();
|
|
612
|
+
const a = createTelegramAdapter(
|
|
613
|
+
{ botToken: BOT_TOKEN, secretToken: SECRET },
|
|
614
|
+
{ apiBaseUrl: "https://test.telegram.local", fetch: f },
|
|
615
|
+
);
|
|
616
|
+
await a.react?.({ event, emoji: "white_check_mark" });
|
|
617
|
+
await a.react?.({ event, emoji: "warning" });
|
|
618
|
+
expect(JSON.parse(String(calls[0]?.init.body)).reaction[0].emoji).toBe("๐");
|
|
619
|
+
expect(JSON.parse(String(calls[1]?.init.body)).reaction[0].emoji).toBe("๐ฑ");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("rejects when chat/message id is not numeric", async () => {
|
|
623
|
+
const { fetch: f } = captureFetch();
|
|
624
|
+
const a = createTelegramAdapter(
|
|
625
|
+
{ botToken: BOT_TOKEN, secretToken: SECRET },
|
|
626
|
+
{ apiBaseUrl: "https://test.telegram.local", fetch: f },
|
|
627
|
+
);
|
|
628
|
+
await expect(a.react?.({ event: { ...event, ts: "nope" }, emoji: "eyes" })).rejects.toThrow(
|
|
629
|
+
TelegramAdapterError,
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("throws on Telegram-side ok:false (router swallows it)", async () => {
|
|
634
|
+
const f = (async () =>
|
|
635
|
+
new Response(JSON.stringify({ ok: false, description: "REACTION_INVALID" }), {
|
|
636
|
+
status: 200,
|
|
637
|
+
headers: { "content-type": "application/json" },
|
|
638
|
+
})) as unknown as typeof fetch;
|
|
639
|
+
const a = createTelegramAdapter(
|
|
640
|
+
{ botToken: BOT_TOKEN, secretToken: SECRET },
|
|
641
|
+
{ apiBaseUrl: "https://test.telegram.local", fetch: f },
|
|
642
|
+
);
|
|
643
|
+
await expect(a.react?.({ event, emoji: "eyes" })).rejects.toThrow(/REACTION_INVALID/);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
571
647
|
describe("apiBaseUrl resolution", () => {
|
|
572
648
|
const event: InboundEvent = {
|
|
573
649
|
idempotencyKey: "1",
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* sendReply: POST to `https://api.telegram.org/bot<token>/sendMessage`.
|
|
22
22
|
* setTyping: POST `sendChatAction` with `action=typing`.
|
|
23
|
+
* react: POST `setMessageReaction` (Bot API 7.0) โ best-effort status emoji.
|
|
23
24
|
*/
|
|
24
25
|
import { CrewhausError } from "@crewhaus/errors";
|
|
25
26
|
import { verifyTelegramSecret } from "./verify.js";
|
|
@@ -75,6 +76,14 @@ export interface ChannelAdapter {
|
|
|
75
76
|
parseInbound(req: RawRequest): ParsedInbound;
|
|
76
77
|
sendReply(args: { event: InboundEvent; text: string }): Promise<void>;
|
|
77
78
|
setTyping(args: { event: InboundEvent }): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Phase 3 ยง3.2 โ add an emoji reaction to an inbound message as a
|
|
81
|
+
* lightweight status acknowledgement (eyes/white_check_mark/warning).
|
|
82
|
+
* Telegram restricts reactions to a fixed emoji set, so the channel-generic
|
|
83
|
+
* status names are mapped to the nearest allowed reaction. Optional โ the
|
|
84
|
+
* session-router skips the hook when an adapter leaves it undefined.
|
|
85
|
+
*/
|
|
86
|
+
react?(args: { event: InboundEvent; emoji: string }): Promise<void>;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
export type TelegramAdapterConfig = {
|
|
@@ -90,6 +99,15 @@ export type TelegramAdapterOptions = {
|
|
|
90
99
|
|
|
91
100
|
const DEFAULT_API_BASE_URL = "https://api.telegram.org";
|
|
92
101
|
|
|
102
|
+
// Telegram's `setMessageReaction` only accepts emoji from a fixed allowed set
|
|
103
|
+
// (โ
and โ ๏ธ are NOT in it), so map the channel-generic status names to the
|
|
104
|
+
// nearest allowed reaction. An unrecognised name is passed through verbatim.
|
|
105
|
+
const TELEGRAM_REACTION_EMOJI: Record<string, string> = {
|
|
106
|
+
eyes: "๐",
|
|
107
|
+
white_check_mark: "๐",
|
|
108
|
+
warning: "๐ฑ",
|
|
109
|
+
};
|
|
110
|
+
|
|
93
111
|
export function createTelegramAdapter(
|
|
94
112
|
config: TelegramAdapterConfig,
|
|
95
113
|
opts: TelegramAdapterOptions = {},
|
|
@@ -242,6 +260,45 @@ export function createTelegramAdapter(
|
|
|
242
260
|
});
|
|
243
261
|
// setTyping is best-effort; a non-200 here should not fail the run.
|
|
244
262
|
},
|
|
263
|
+
|
|
264
|
+
// Phase 3 ยง3.2 โ emoji reactions via Bot API 7.0 `setMessageReaction`.
|
|
265
|
+
// chat_id comes from the raw chat id (workspaceId); message_id from `ts`.
|
|
266
|
+
async react(args: { event: InboundEvent; emoji: string }): Promise<void> {
|
|
267
|
+
const url = botEndpoint("setMessageReaction");
|
|
268
|
+
const chatId = Number.parseInt(args.event.workspaceId, 10);
|
|
269
|
+
const messageId = Number.parseInt(args.event.ts, 10);
|
|
270
|
+
if (!Number.isFinite(chatId) || !Number.isFinite(messageId)) {
|
|
271
|
+
throw new TelegramAdapterError(
|
|
272
|
+
`invalid chat/message id for reaction: ${args.event.workspaceId}/${args.event.ts}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const emoji = TELEGRAM_REACTION_EMOJI[args.emoji] ?? args.emoji;
|
|
276
|
+
const res = await doFetch(url, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: { "Content-Type": "application/json" },
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
chat_id: chatId,
|
|
281
|
+
message_id: messageId,
|
|
282
|
+
reaction: [{ type: "emoji", emoji }],
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
// Reactions are best-effort โ the session-router catches and continues so
|
|
286
|
+
// a flaky/disallowed reaction never aborts message processing.
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
throw new TelegramAdapterError(
|
|
289
|
+
`setMessageReaction failed: ${res.status} ${res.statusText}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
293
|
+
if (ct.includes("application/json")) {
|
|
294
|
+
const json = (await res.json()) as { ok?: boolean; description?: string };
|
|
295
|
+
if (json.ok === false) {
|
|
296
|
+
throw new TelegramAdapterError(
|
|
297
|
+
`setMessageReaction error: ${json.description ?? "unknown"}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
},
|
|
245
302
|
};
|
|
246
303
|
}
|
|
247
304
|
|