agent-messenger 2.12.0 → 2.12.2

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.
Files changed (121) hide show
  1. package/.claude-plugin/README.md +11 -1
  2. package/.claude-plugin/marketplace.json +14 -1
  3. package/.claude-plugin/plugin.json +4 -2
  4. package/CONTRIBUTING.md +12 -0
  5. package/README.md +30 -4
  6. package/dist/package.json +10 -2
  7. package/dist/src/cli.d.ts.map +1 -1
  8. package/dist/src/cli.js +3 -0
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/platforms/kakaotalk/client.d.ts +22 -0
  11. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  12. package/dist/src/platforms/kakaotalk/client.js +93 -7
  13. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  14. package/dist/src/platforms/kakaotalk/listener.d.ts +4 -7
  15. package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
  16. package/dist/src/platforms/kakaotalk/listener.js +43 -72
  17. package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
  18. package/dist/src/platforms/telegrambot/cli.d.ts +5 -0
  19. package/dist/src/platforms/telegrambot/cli.d.ts.map +1 -0
  20. package/dist/src/platforms/telegrambot/cli.js +29 -0
  21. package/dist/src/platforms/telegrambot/cli.js.map +1 -0
  22. package/dist/src/platforms/telegrambot/client.d.ts +85 -0
  23. package/dist/src/platforms/telegrambot/client.d.ts.map +1 -0
  24. package/dist/src/platforms/telegrambot/client.js +282 -0
  25. package/dist/src/platforms/telegrambot/client.js.map +1 -0
  26. package/dist/src/platforms/telegrambot/commands/auth.d.ts +31 -0
  27. package/dist/src/platforms/telegrambot/commands/auth.d.ts.map +1 -0
  28. package/dist/src/platforms/telegrambot/commands/auth.js +173 -0
  29. package/dist/src/platforms/telegrambot/commands/auth.js.map +1 -0
  30. package/dist/src/platforms/telegrambot/commands/chat.d.ts +25 -0
  31. package/dist/src/platforms/telegrambot/commands/chat.d.ts.map +1 -0
  32. package/dist/src/platforms/telegrambot/commands/chat.js +69 -0
  33. package/dist/src/platforms/telegrambot/commands/chat.js.map +1 -0
  34. package/dist/src/platforms/telegrambot/commands/index.d.ts +6 -0
  35. package/dist/src/platforms/telegrambot/commands/index.d.ts.map +1 -0
  36. package/dist/src/platforms/telegrambot/commands/index.js +6 -0
  37. package/dist/src/platforms/telegrambot/commands/index.js.map +1 -0
  38. package/dist/src/platforms/telegrambot/commands/message.d.ts +39 -0
  39. package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -0
  40. package/dist/src/platforms/telegrambot/commands/message.js +145 -0
  41. package/dist/src/platforms/telegrambot/commands/message.js.map +1 -0
  42. package/dist/src/platforms/telegrambot/commands/reaction.d.ts +16 -0
  43. package/dist/src/platforms/telegrambot/commands/reaction.d.ts.map +1 -0
  44. package/dist/src/platforms/telegrambot/commands/reaction.js +49 -0
  45. package/dist/src/platforms/telegrambot/commands/reaction.js.map +1 -0
  46. package/dist/src/platforms/telegrambot/commands/shared.d.ts +12 -0
  47. package/dist/src/platforms/telegrambot/commands/shared.d.ts.map +1 -0
  48. package/dist/src/platforms/telegrambot/commands/shared.js +21 -0
  49. package/dist/src/platforms/telegrambot/commands/shared.js.map +1 -0
  50. package/dist/src/platforms/telegrambot/commands/whoami.d.ts +17 -0
  51. package/dist/src/platforms/telegrambot/commands/whoami.d.ts.map +1 -0
  52. package/dist/src/platforms/telegrambot/commands/whoami.js +30 -0
  53. package/dist/src/platforms/telegrambot/commands/whoami.js.map +1 -0
  54. package/dist/src/platforms/telegrambot/credential-manager.d.ts +17 -0
  55. package/dist/src/platforms/telegrambot/credential-manager.d.ts.map +1 -0
  56. package/dist/src/platforms/telegrambot/credential-manager.js +113 -0
  57. package/dist/src/platforms/telegrambot/credential-manager.js.map +1 -0
  58. package/dist/src/platforms/telegrambot/index.d.ts +7 -0
  59. package/dist/src/platforms/telegrambot/index.d.ts.map +1 -0
  60. package/dist/src/platforms/telegrambot/index.js +5 -0
  61. package/dist/src/platforms/telegrambot/index.js.map +1 -0
  62. package/dist/src/platforms/telegrambot/listener.d.ts +30 -0
  63. package/dist/src/platforms/telegrambot/listener.d.ts.map +1 -0
  64. package/dist/src/platforms/telegrambot/listener.js +186 -0
  65. package/dist/src/platforms/telegrambot/listener.js.map +1 -0
  66. package/dist/src/platforms/telegrambot/types.d.ts +256 -0
  67. package/dist/src/platforms/telegrambot/types.d.ts.map +1 -0
  68. package/dist/src/platforms/telegrambot/types.js +96 -0
  69. package/dist/src/platforms/telegrambot/types.js.map +1 -0
  70. package/docs/content/docs/cli/meta.json +1 -0
  71. package/docs/content/docs/cli/telegrambot.mdx +149 -0
  72. package/docs/content/docs/index.mdx +10 -9
  73. package/docs/content/docs/quick-start.mdx +2 -0
  74. package/docs/content/docs/sdk/meta.json +1 -0
  75. package/docs/content/docs/sdk/telegrambot.mdx +216 -0
  76. package/e2e/config.ts +24 -0
  77. package/e2e/helpers.ts +1 -0
  78. package/e2e/telegrambot.e2e.test.ts +185 -0
  79. package/examples/telegrambot-listen.ts +54 -0
  80. package/package.json +10 -2
  81. package/scripts/postbuild.ts +1 -0
  82. package/skills/agent-channeltalk/SKILL.md +1 -1
  83. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  84. package/skills/agent-discord/SKILL.md +1 -1
  85. package/skills/agent-discordbot/SKILL.md +1 -1
  86. package/skills/agent-instagram/SKILL.md +1 -1
  87. package/skills/agent-kakaotalk/SKILL.md +12 -5
  88. package/skills/agent-line/SKILL.md +1 -1
  89. package/skills/agent-slack/SKILL.md +1 -1
  90. package/skills/agent-slackbot/SKILL.md +1 -1
  91. package/skills/agent-teams/SKILL.md +1 -1
  92. package/skills/agent-telegram/SKILL.md +1 -1
  93. package/skills/agent-telegrambot/SKILL.md +357 -0
  94. package/skills/agent-webex/SKILL.md +1 -1
  95. package/skills/agent-wechatbot/SKILL.md +1 -1
  96. package/skills/agent-whatsapp/SKILL.md +1 -1
  97. package/skills/agent-whatsappbot/SKILL.md +1 -1
  98. package/src/cli.ts +4 -0
  99. package/src/platforms/kakaotalk/client-listener-integration.test.ts +411 -0
  100. package/src/platforms/kakaotalk/client.test.ts +3 -0
  101. package/src/platforms/kakaotalk/client.ts +108 -9
  102. package/src/platforms/kakaotalk/listener.test.ts +155 -149
  103. package/src/platforms/kakaotalk/listener.ts +46 -80
  104. package/src/platforms/telegrambot/cli.ts +34 -0
  105. package/src/platforms/telegrambot/client.test.ts +454 -0
  106. package/src/platforms/telegrambot/client.ts +404 -0
  107. package/src/platforms/telegrambot/commands/auth.test.ts +244 -0
  108. package/src/platforms/telegrambot/commands/auth.ts +220 -0
  109. package/src/platforms/telegrambot/commands/chat.ts +96 -0
  110. package/src/platforms/telegrambot/commands/index.ts +5 -0
  111. package/src/platforms/telegrambot/commands/message.ts +235 -0
  112. package/src/platforms/telegrambot/commands/reaction.ts +70 -0
  113. package/src/platforms/telegrambot/commands/shared.ts +32 -0
  114. package/src/platforms/telegrambot/commands/whoami.ts +45 -0
  115. package/src/platforms/telegrambot/credential-manager.test.ts +196 -0
  116. package/src/platforms/telegrambot/credential-manager.ts +141 -0
  117. package/src/platforms/telegrambot/index.ts +44 -0
  118. package/src/platforms/telegrambot/listener.test.ts +398 -0
  119. package/src/platforms/telegrambot/listener.ts +198 -0
  120. package/src/platforms/telegrambot/types.test.ts +128 -0
  121. package/src/platforms/telegrambot/types.ts +282 -0
@@ -0,0 +1,398 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
+
3
+ import { TelegramBotClient } from './client'
4
+ import { TelegramBotListener } from './listener'
5
+ import type { TelegramBotUser, TelegramUpdate } from './types'
6
+ import { TelegramBotError } from './types'
7
+
8
+ interface FakeClient {
9
+ deleteWebhook: () => Promise<boolean>
10
+ getMe: () => Promise<TelegramBotUser>
11
+ getUpdates: (options?: unknown, signal?: AbortSignal) => Promise<TelegramUpdate[]>
12
+ }
13
+
14
+ const ME: TelegramBotUser = {
15
+ id: 1,
16
+ is_bot: true,
17
+ first_name: 'Test Bot',
18
+ username: 'testbot',
19
+ }
20
+
21
+ function makeFakeClient(overrides: Partial<FakeClient> = {}): TelegramBotClient {
22
+ const base: FakeClient = {
23
+ deleteWebhook: async () => true,
24
+ getMe: async () => ME,
25
+ getUpdates: async () => [],
26
+ ...overrides,
27
+ }
28
+ return base as unknown as TelegramBotClient
29
+ }
30
+
31
+ function flush(ms = 5): Promise<void> {
32
+ return new Promise((resolve) => setTimeout(resolve, ms))
33
+ }
34
+
35
+ function pendingWithAbort(signal?: AbortSignal): Promise<never> {
36
+ return new Promise<never>((_resolve, reject) => {
37
+ if (signal?.aborted) {
38
+ reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }))
39
+ return
40
+ }
41
+ signal?.addEventListener('abort', () => reject(Object.assign(new Error('Aborted'), { name: 'AbortError' })), {
42
+ once: true,
43
+ })
44
+ })
45
+ }
46
+
47
+ describe('TelegramBotListener', () => {
48
+ let listener: TelegramBotListener | null = null
49
+
50
+ beforeEach(() => {
51
+ listener = null
52
+ })
53
+
54
+ afterEach(() => {
55
+ listener?.stop()
56
+ })
57
+
58
+ it('emits "connected" with bot user after start()', async () => {
59
+ let connectedUser: TelegramBotUser | null = null
60
+ const client = makeFakeClient({
61
+ getUpdates: (_options, signal) => pendingWithAbort(signal),
62
+ })
63
+
64
+ listener = new TelegramBotListener(client)
65
+ listener.on('connected', ({ user }) => {
66
+ connectedUser = user
67
+ })
68
+
69
+ await listener.start()
70
+ await flush()
71
+
72
+ expect(connectedUser).not.toBeNull()
73
+ expect(connectedUser!.username).toBe('testbot')
74
+ })
75
+
76
+ it('deletes webhook before polling and forwards dropPendingUpdates', async () => {
77
+ let deleteWebhookCalls: Array<{ drop_pending_updates?: boolean } | undefined> = []
78
+ const client = {
79
+ deleteWebhook: async (opts?: { drop_pending_updates?: boolean }) => {
80
+ deleteWebhookCalls.push(opts)
81
+ return true
82
+ },
83
+ getMe: async () => ME,
84
+ getUpdates: (_options: unknown, signal?: AbortSignal) => pendingWithAbort(signal),
85
+ } as unknown as TelegramBotClient
86
+
87
+ listener = new TelegramBotListener(client, { dropPendingUpdates: true })
88
+ await listener.start()
89
+ await flush()
90
+
91
+ expect(deleteWebhookCalls).toHaveLength(1)
92
+ expect(deleteWebhookCalls[0]).toEqual({ drop_pending_updates: true })
93
+ })
94
+
95
+ it('dispatches message updates', async () => {
96
+ const messages: string[] = []
97
+ let pollCount = 0
98
+
99
+ const client = makeFakeClient({
100
+ getUpdates: (_options, signal) => {
101
+ pollCount++
102
+ if (pollCount === 1) {
103
+ return Promise.resolve([
104
+ {
105
+ update_id: 100,
106
+ message: {
107
+ message_id: 1,
108
+ date: 1,
109
+ chat: { id: 99, type: 'private' as const, first_name: 'Alice' },
110
+ text: 'hello',
111
+ },
112
+ },
113
+ ])
114
+ }
115
+ return pendingWithAbort(signal)
116
+ },
117
+ })
118
+
119
+ listener = new TelegramBotListener(client)
120
+ listener.on('message', (msg) => {
121
+ if (msg.text) messages.push(msg.text)
122
+ })
123
+
124
+ await listener.start()
125
+ await flush(20)
126
+
127
+ expect(messages).toEqual(['hello'])
128
+ })
129
+
130
+ it('advances offset to update_id + 1 after processing', async () => {
131
+ const offsetsSeen: Array<number | undefined> = []
132
+ let pollCount = 0
133
+
134
+ const client = makeFakeClient({
135
+ getUpdates: (options, signal) => {
136
+ pollCount++
137
+ const offset = (options as { offset?: number } | undefined)?.offset
138
+ offsetsSeen.push(offset)
139
+ if (pollCount === 1) {
140
+ return Promise.resolve([
141
+ {
142
+ update_id: 100,
143
+ message: {
144
+ message_id: 1,
145
+ date: 1,
146
+ chat: { id: 99, type: 'private' as const, first_name: 'A' },
147
+ text: 'a',
148
+ },
149
+ },
150
+ {
151
+ update_id: 101,
152
+ message: {
153
+ message_id: 2,
154
+ date: 2,
155
+ chat: { id: 99, type: 'private' as const, first_name: 'A' },
156
+ text: 'b',
157
+ },
158
+ },
159
+ ])
160
+ }
161
+ return pendingWithAbort(signal)
162
+ },
163
+ })
164
+
165
+ listener = new TelegramBotListener(client)
166
+ await listener.start()
167
+ await flush(20)
168
+
169
+ expect(offsetsSeen[0]).toBe(0)
170
+ expect(offsetsSeen[1]).toBe(102)
171
+ })
172
+
173
+ it('emits telegram_update for every update (catch-all)', async () => {
174
+ let count = 0
175
+ let pollCount = 0
176
+
177
+ const client = makeFakeClient({
178
+ getUpdates: async () => {
179
+ pollCount++
180
+ if (pollCount === 1) {
181
+ return [{ update_id: 1, callback_query: { id: 'q', from: ME, chat_instance: 'ci', data: 'click' } }]
182
+ }
183
+ return new Promise(() => {})
184
+ },
185
+ })
186
+
187
+ listener = new TelegramBotListener(client)
188
+ listener.on('telegram_update', () => {
189
+ count++
190
+ })
191
+
192
+ await listener.start()
193
+ await flush(20)
194
+
195
+ expect(count).toBe(1)
196
+ })
197
+
198
+ it('stops on fatal Unauthorized error', async () => {
199
+ let errorCaught: Error | null = null
200
+ const client = makeFakeClient({
201
+ getUpdates: async () => {
202
+ throw new TelegramBotError('Unauthorized', 'unauthorized')
203
+ },
204
+ })
205
+
206
+ listener = new TelegramBotListener(client)
207
+ listener.on('error', (err) => {
208
+ errorCaught = err
209
+ })
210
+
211
+ await listener.start()
212
+ await flush(20)
213
+
214
+ expect(errorCaught).not.toBeNull()
215
+ expect((errorCaught as unknown as TelegramBotError).code).toBe('unauthorized')
216
+ })
217
+
218
+ it('emits disconnected on transient errors', async () => {
219
+ let disconnectedCount = 0
220
+ let pollCount = 0
221
+
222
+ const client = makeFakeClient({
223
+ getUpdates: (_options, signal) => {
224
+ pollCount++
225
+ if (pollCount === 1) {
226
+ return Promise.reject(new Error('Network error'))
227
+ }
228
+ return pendingWithAbort(signal)
229
+ },
230
+ })
231
+
232
+ listener = new TelegramBotListener(client)
233
+ listener.on('disconnected', () => {
234
+ disconnectedCount++
235
+ })
236
+
237
+ await listener.start()
238
+ await flush(50)
239
+
240
+ expect(disconnectedCount).toBeGreaterThanOrEqual(1)
241
+ })
242
+
243
+ it('stop() aborts in-flight getUpdates and halts polling', async () => {
244
+ let pollStarts = 0
245
+ let aborted = false
246
+
247
+ const client = makeFakeClient({
248
+ getUpdates: (_options, signal) => {
249
+ pollStarts++
250
+ return new Promise<never>((_resolve, reject) => {
251
+ signal?.addEventListener(
252
+ 'abort',
253
+ () => {
254
+ aborted = true
255
+ reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }))
256
+ },
257
+ { once: true },
258
+ )
259
+ })
260
+ },
261
+ })
262
+
263
+ listener = new TelegramBotListener(client)
264
+ await listener.start()
265
+ await flush()
266
+
267
+ expect(pollStarts).toBe(1)
268
+
269
+ listener.stop()
270
+ await flush(20)
271
+
272
+ expect(aborted).toBe(true)
273
+ expect(pollStarts).toBe(1)
274
+ })
275
+
276
+ it('on/off/once chain returns this', () => {
277
+ const client = makeFakeClient()
278
+ listener = new TelegramBotListener(client)
279
+ const fn = (): void => {}
280
+ expect(listener.on('message', fn)).toBe(listener)
281
+ expect(listener.off('message', fn)).toBe(listener)
282
+ expect(listener.once('message', fn)).toBe(listener)
283
+ })
284
+
285
+ it('emits chat_member and my_chat_member as distinct events', async () => {
286
+ const myChatMemberEvents: unknown[] = []
287
+ const chatMemberEvents: unknown[] = []
288
+ let pollCount = 0
289
+
290
+ const memberPayload = {
291
+ chat: { id: 1, type: 'private' as const, first_name: 'A' },
292
+ from: ME,
293
+ date: 1,
294
+ old_chat_member: { user: ME, status: 'member' as const },
295
+ new_chat_member: { user: ME, status: 'administrator' as const },
296
+ }
297
+
298
+ const client = makeFakeClient({
299
+ getUpdates: (_options, signal) => {
300
+ pollCount++
301
+ if (pollCount === 1) {
302
+ return Promise.resolve([
303
+ { update_id: 1, my_chat_member: memberPayload },
304
+ { update_id: 2, chat_member: memberPayload },
305
+ ])
306
+ }
307
+ return pendingWithAbort(signal)
308
+ },
309
+ })
310
+
311
+ listener = new TelegramBotListener(client)
312
+ listener.on('my_chat_member', (e) => myChatMemberEvents.push(e))
313
+ listener.on('chat_member', (e) => chatMemberEvents.push(e))
314
+
315
+ await listener.start()
316
+ await flush(20)
317
+
318
+ expect(myChatMemberEvents).toHaveLength(1)
319
+ expect(chatMemberEvents).toHaveLength(1)
320
+ })
321
+
322
+ it('user handler exception does not stop polling and surfaces via error event', async () => {
323
+ let messageCount = 0
324
+ let errorCount = 0
325
+ let pollCount = 0
326
+
327
+ const client = makeFakeClient({
328
+ getUpdates: (_options, signal) => {
329
+ pollCount++
330
+ if (pollCount === 1) {
331
+ return Promise.resolve([
332
+ {
333
+ update_id: 1,
334
+ message: {
335
+ message_id: 1,
336
+ date: 1,
337
+ chat: { id: 1, type: 'private' as const, first_name: 'A' },
338
+ text: 'first',
339
+ },
340
+ },
341
+ {
342
+ update_id: 2,
343
+ message: {
344
+ message_id: 2,
345
+ date: 2,
346
+ chat: { id: 1, type: 'private' as const, first_name: 'A' },
347
+ text: 'second',
348
+ },
349
+ },
350
+ ])
351
+ }
352
+ return pendingWithAbort(signal)
353
+ },
354
+ })
355
+
356
+ listener = new TelegramBotListener(client)
357
+ listener.on('message', (msg) => {
358
+ messageCount++
359
+ if (msg.text === 'first') {
360
+ throw new Error('handler boom')
361
+ }
362
+ })
363
+ listener.on('error', () => {
364
+ errorCount++
365
+ })
366
+
367
+ await listener.start()
368
+ await flush(20)
369
+
370
+ expect(messageCount).toBe(2)
371
+ expect(errorCount).toBeGreaterThanOrEqual(1)
372
+ })
373
+
374
+ it('propagates AbortError from getUpdates through the listener cleanly', async () => {
375
+ let aborted = false
376
+ const client = makeFakeClient({
377
+ getUpdates: (_options, signal) =>
378
+ new Promise<never>((_resolve, reject) => {
379
+ signal?.addEventListener(
380
+ 'abort',
381
+ () => {
382
+ aborted = true
383
+ reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }))
384
+ },
385
+ { once: true },
386
+ )
387
+ }),
388
+ })
389
+
390
+ listener = new TelegramBotListener(client)
391
+ await listener.start()
392
+ await flush(20)
393
+
394
+ listener.stop()
395
+ await flush(20)
396
+ expect(aborted).toBe(true)
397
+ })
398
+ })
@@ -0,0 +1,198 @@
1
+ import { EventEmitter } from 'events'
2
+
3
+ import type { TelegramBotClient } from './client'
4
+ import type { TelegramBotListenerEventMap, TelegramBotListenerOptions, TelegramBotUser, TelegramUpdate } from './types'
5
+ import { TelegramBotError } from './types'
6
+
7
+ const DEFAULT_TIMEOUT_SECONDS = 30
8
+ const DEFAULT_LIMIT = 100
9
+ const FETCH_TIMEOUT_GRACE_MS = 10_000
10
+ const RECONNECT_BASE_DELAY = 1_000
11
+ const RECONNECT_MAX_DELAY = 30_000
12
+ const FATAL_ERROR_CODES = new Set(['unauthorized', 'conflict'])
13
+
14
+ type EventKey = keyof TelegramBotListenerEventMap
15
+
16
+ export class TelegramBotListener {
17
+ private client: TelegramBotClient
18
+ private timeoutSeconds: number
19
+ private limit: number
20
+ private allowedUpdates: string[] | undefined
21
+ private dropPendingUpdates: boolean
22
+ private running = false
23
+ private offset = 0
24
+ private reconnectAttempts = 0
25
+ private emitter = new EventEmitter()
26
+ private abortController: AbortController | null = null
27
+ private cachedUser: TelegramBotUser | null = null
28
+ private generation = 0
29
+
30
+ constructor(client: TelegramBotClient, options?: TelegramBotListenerOptions) {
31
+ this.client = client
32
+ this.timeoutSeconds = options?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS
33
+ this.limit = options?.limit ?? DEFAULT_LIMIT
34
+ this.allowedUpdates = options?.allowedUpdates
35
+ this.dropPendingUpdates = options?.dropPendingUpdates ?? false
36
+ }
37
+
38
+ async start(): Promise<void> {
39
+ if (this.running) return
40
+ this.running = true
41
+ this.reconnectAttempts = 0
42
+ this.generation++
43
+ const generation = this.generation
44
+
45
+ try {
46
+ await this.client.deleteWebhook({ drop_pending_updates: this.dropPendingUpdates })
47
+ } catch (error) {
48
+ if (!this.isCurrent(generation)) return
49
+ this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
50
+ this.running = false
51
+ return
52
+ }
53
+
54
+ if (!this.isCurrent(generation)) return
55
+
56
+ try {
57
+ this.cachedUser = await this.client.getMe()
58
+ if (!this.isCurrent(generation)) return
59
+ this.emitter.emit('connected', { user: this.cachedUser })
60
+ } catch (error) {
61
+ if (!this.isCurrent(generation)) return
62
+ this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
63
+ this.running = false
64
+ return
65
+ }
66
+
67
+ void this.pollLoop(generation)
68
+ }
69
+
70
+ stop(): void {
71
+ this.running = false
72
+ this.generation++
73
+ if (this.abortController) {
74
+ this.abortController.abort()
75
+ this.abortController = null
76
+ }
77
+ this.cachedUser = null
78
+ }
79
+
80
+ on<K extends EventKey>(event: K, listener: (...args: TelegramBotListenerEventMap[K]) => void): this {
81
+ this.emitter.on(event, listener as (...args: unknown[]) => void)
82
+ return this
83
+ }
84
+
85
+ off<K extends EventKey>(event: K, listener: (...args: TelegramBotListenerEventMap[K]) => void): this {
86
+ this.emitter.off(event, listener as (...args: unknown[]) => void)
87
+ return this
88
+ }
89
+
90
+ once<K extends EventKey>(event: K, listener: (...args: TelegramBotListenerEventMap[K]) => void): this {
91
+ this.emitter.once(event, listener as (...args: unknown[]) => void)
92
+ return this
93
+ }
94
+
95
+ private isCurrent(generation: number): boolean {
96
+ return generation === this.generation && this.running
97
+ }
98
+
99
+ private async pollLoop(generation: number): Promise<void> {
100
+ let firstPoll = true
101
+
102
+ while (this.isCurrent(generation)) {
103
+ const pollController = new AbortController()
104
+ this.abortController = pollController
105
+
106
+ // Server-side getUpdates uses `timeout` (seconds); add grace so the HTTP fetch can't hang
107
+ // past the long-poll deadline if the underlying socket stalls.
108
+ const fetchTimeout = setTimeout(
109
+ () => pollController.abort(),
110
+ (this.timeoutSeconds + FETCH_TIMEOUT_GRACE_MS / 1000) * 1000,
111
+ )
112
+
113
+ let updates: TelegramUpdate[]
114
+ try {
115
+ updates = await this.client.getUpdates(
116
+ {
117
+ offset: this.offset,
118
+ limit: this.limit,
119
+ timeout: this.timeoutSeconds,
120
+ allowed_updates: firstPoll ? this.allowedUpdates : undefined,
121
+ },
122
+ pollController.signal,
123
+ )
124
+ firstPoll = false
125
+ this.reconnectAttempts = 0
126
+ } catch (error) {
127
+ clearTimeout(fetchTimeout)
128
+ if (pollController.signal.aborted && !this.isCurrent(generation)) {
129
+ return
130
+ }
131
+ if (!this.isCurrent(generation)) return
132
+
133
+ if (error instanceof TelegramBotError && FATAL_ERROR_CODES.has(error.code)) {
134
+ this.emitter.emit('error', error)
135
+ this.running = false
136
+ return
137
+ }
138
+
139
+ this.emitter.emit('disconnected')
140
+ await this.backoff(generation)
141
+ continue
142
+ }
143
+ clearTimeout(fetchTimeout)
144
+
145
+ if (!this.isCurrent(generation)) return
146
+
147
+ for (const update of updates) {
148
+ if (!this.isCurrent(generation)) return
149
+ // Advance offset BEFORE dispatching so a thrown user handler doesn't cause redelivery.
150
+ this.offset = update.update_id + 1
151
+ this.dispatch(update)
152
+ }
153
+ }
154
+ }
155
+
156
+ private dispatch(update: TelegramUpdate): void {
157
+ if (update.message) this.safeEmit('message', update.message)
158
+ if (update.edited_message) this.safeEmit('edited_message', update.edited_message)
159
+ if (update.channel_post) this.safeEmit('channel_post', update.channel_post)
160
+ if (update.edited_channel_post) this.safeEmit('edited_channel_post', update.edited_channel_post)
161
+ if (update.callback_query) this.safeEmit('callback_query', update.callback_query)
162
+ if (update.inline_query) this.safeEmit('inline_query', update.inline_query)
163
+ if (update.my_chat_member) this.safeEmit('my_chat_member', update.my_chat_member)
164
+ if (update.chat_member) this.safeEmit('chat_member', update.chat_member)
165
+ this.safeEmit('telegram_update', update)
166
+ }
167
+
168
+ private safeEmit<K extends EventKey>(event: K, ...args: TelegramBotListenerEventMap[K]): void {
169
+ try {
170
+ this.emitter.emit(event, ...args)
171
+ } catch (handlerError) {
172
+ const err = handlerError instanceof Error ? handlerError : new Error(String(handlerError))
173
+ try {
174
+ this.emitter.emit('error', err)
175
+ } catch {
176
+ // Swallow secondary errors from error handlers to keep the poll loop alive.
177
+ }
178
+ }
179
+ }
180
+
181
+ private async backoff(generation: number): Promise<void> {
182
+ const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** this.reconnectAttempts, RECONNECT_MAX_DELAY)
183
+ this.reconnectAttempts++
184
+ await new Promise<void>((resolve) => {
185
+ const timer = setTimeout(() => resolve(), delay)
186
+ const onAbort = (): void => {
187
+ clearTimeout(timer)
188
+ resolve()
189
+ }
190
+ if (this.isCurrent(generation) && this.abortController) {
191
+ this.abortController.signal.addEventListener('abort', onAbort, { once: true })
192
+ } else {
193
+ clearTimeout(timer)
194
+ resolve()
195
+ }
196
+ })
197
+ }
198
+ }