agent-messenger 2.11.2 → 2.12.1

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 (172) 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 +41 -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/channeltalk/credential-manager.js +2 -2
  11. package/dist/src/platforms/channeltalk/credential-manager.js.map +1 -1
  12. package/dist/src/platforms/channeltalkbot/credential-manager.js +2 -2
  13. package/dist/src/platforms/channeltalkbot/credential-manager.js.map +1 -1
  14. package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
  15. package/dist/src/platforms/discord/credential-manager.js +2 -2
  16. package/dist/src/platforms/discord/credential-manager.js.map +1 -1
  17. package/dist/src/platforms/discordbot/credential-manager.js +2 -2
  18. package/dist/src/platforms/discordbot/credential-manager.js.map +1 -1
  19. package/dist/src/platforms/instagram/credential-manager.js +2 -2
  20. package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
  21. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  22. package/dist/src/platforms/kakaotalk/client.js +3 -4
  23. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  24. package/dist/src/platforms/kakaotalk/credential-manager.js +2 -2
  25. package/dist/src/platforms/kakaotalk/credential-manager.js.map +1 -1
  26. package/dist/src/platforms/line/client.js +2 -2
  27. package/dist/src/platforms/line/client.js.map +1 -1
  28. package/dist/src/platforms/line/credential-manager.js +2 -2
  29. package/dist/src/platforms/line/credential-manager.js.map +1 -1
  30. package/dist/src/platforms/slack/credential-manager.js +2 -2
  31. package/dist/src/platforms/slack/credential-manager.js.map +1 -1
  32. package/dist/src/platforms/slackbot/credential-manager.js +2 -2
  33. package/dist/src/platforms/slackbot/credential-manager.js.map +1 -1
  34. package/dist/src/platforms/teams/credential-manager.js +2 -2
  35. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  36. package/dist/src/platforms/telegram/credential-manager.js +2 -2
  37. package/dist/src/platforms/telegram/credential-manager.js.map +1 -1
  38. package/dist/src/platforms/telegrambot/cli.d.ts +5 -0
  39. package/dist/src/platforms/telegrambot/cli.d.ts.map +1 -0
  40. package/dist/src/platforms/telegrambot/cli.js +29 -0
  41. package/dist/src/platforms/telegrambot/cli.js.map +1 -0
  42. package/dist/src/platforms/telegrambot/client.d.ts +85 -0
  43. package/dist/src/platforms/telegrambot/client.d.ts.map +1 -0
  44. package/dist/src/platforms/telegrambot/client.js +282 -0
  45. package/dist/src/platforms/telegrambot/client.js.map +1 -0
  46. package/dist/src/platforms/telegrambot/commands/auth.d.ts +31 -0
  47. package/dist/src/platforms/telegrambot/commands/auth.d.ts.map +1 -0
  48. package/dist/src/platforms/telegrambot/commands/auth.js +173 -0
  49. package/dist/src/platforms/telegrambot/commands/auth.js.map +1 -0
  50. package/dist/src/platforms/telegrambot/commands/chat.d.ts +25 -0
  51. package/dist/src/platforms/telegrambot/commands/chat.d.ts.map +1 -0
  52. package/dist/src/platforms/telegrambot/commands/chat.js +69 -0
  53. package/dist/src/platforms/telegrambot/commands/chat.js.map +1 -0
  54. package/dist/src/platforms/telegrambot/commands/index.d.ts +6 -0
  55. package/dist/src/platforms/telegrambot/commands/index.d.ts.map +1 -0
  56. package/dist/src/platforms/telegrambot/commands/index.js +6 -0
  57. package/dist/src/platforms/telegrambot/commands/index.js.map +1 -0
  58. package/dist/src/platforms/telegrambot/commands/message.d.ts +39 -0
  59. package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -0
  60. package/dist/src/platforms/telegrambot/commands/message.js +145 -0
  61. package/dist/src/platforms/telegrambot/commands/message.js.map +1 -0
  62. package/dist/src/platforms/telegrambot/commands/reaction.d.ts +16 -0
  63. package/dist/src/platforms/telegrambot/commands/reaction.d.ts.map +1 -0
  64. package/dist/src/platforms/telegrambot/commands/reaction.js +49 -0
  65. package/dist/src/platforms/telegrambot/commands/reaction.js.map +1 -0
  66. package/dist/src/platforms/telegrambot/commands/shared.d.ts +12 -0
  67. package/dist/src/platforms/telegrambot/commands/shared.d.ts.map +1 -0
  68. package/dist/src/platforms/telegrambot/commands/shared.js +21 -0
  69. package/dist/src/platforms/telegrambot/commands/shared.js.map +1 -0
  70. package/dist/src/platforms/telegrambot/commands/whoami.d.ts +17 -0
  71. package/dist/src/platforms/telegrambot/commands/whoami.d.ts.map +1 -0
  72. package/dist/src/platforms/telegrambot/commands/whoami.js +30 -0
  73. package/dist/src/platforms/telegrambot/commands/whoami.js.map +1 -0
  74. package/dist/src/platforms/telegrambot/credential-manager.d.ts +17 -0
  75. package/dist/src/platforms/telegrambot/credential-manager.d.ts.map +1 -0
  76. package/dist/src/platforms/telegrambot/credential-manager.js +113 -0
  77. package/dist/src/platforms/telegrambot/credential-manager.js.map +1 -0
  78. package/dist/src/platforms/telegrambot/index.d.ts +7 -0
  79. package/dist/src/platforms/telegrambot/index.d.ts.map +1 -0
  80. package/dist/src/platforms/telegrambot/index.js +5 -0
  81. package/dist/src/platforms/telegrambot/index.js.map +1 -0
  82. package/dist/src/platforms/telegrambot/listener.d.ts +30 -0
  83. package/dist/src/platforms/telegrambot/listener.d.ts.map +1 -0
  84. package/dist/src/platforms/telegrambot/listener.js +186 -0
  85. package/dist/src/platforms/telegrambot/listener.js.map +1 -0
  86. package/dist/src/platforms/telegrambot/types.d.ts +256 -0
  87. package/dist/src/platforms/telegrambot/types.d.ts.map +1 -0
  88. package/dist/src/platforms/telegrambot/types.js +96 -0
  89. package/dist/src/platforms/telegrambot/types.js.map +1 -0
  90. package/dist/src/platforms/webex/credential-manager.js +2 -2
  91. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  92. package/dist/src/platforms/wechatbot/credential-manager.js +2 -2
  93. package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -1
  94. package/dist/src/platforms/whatsapp/credential-manager.js +2 -2
  95. package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
  96. package/dist/src/platforms/whatsappbot/credential-manager.js +2 -2
  97. package/dist/src/platforms/whatsappbot/credential-manager.js.map +1 -1
  98. package/dist/src/shared/utils/config-dir.d.ts +14 -0
  99. package/dist/src/shared/utils/config-dir.d.ts.map +1 -0
  100. package/dist/src/shared/utils/config-dir.js +22 -0
  101. package/dist/src/shared/utils/config-dir.js.map +1 -0
  102. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  103. package/dist/src/shared/utils/derived-key-cache.js +2 -2
  104. package/dist/src/shared/utils/derived-key-cache.js.map +1 -1
  105. package/docs/content/docs/cli/meta.json +1 -0
  106. package/docs/content/docs/cli/telegrambot.mdx +149 -0
  107. package/docs/content/docs/index.mdx +10 -9
  108. package/docs/content/docs/quick-start.mdx +2 -0
  109. package/docs/content/docs/sdk/meta.json +1 -0
  110. package/docs/content/docs/sdk/telegrambot.mdx +216 -0
  111. package/e2e/config.ts +24 -0
  112. package/e2e/helpers.ts +1 -0
  113. package/e2e/telegrambot.e2e.test.ts +185 -0
  114. package/examples/telegrambot-listen.ts +54 -0
  115. package/package.json +10 -2
  116. package/scripts/postbuild.ts +1 -0
  117. package/skills/agent-channeltalk/SKILL.md +1 -1
  118. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  119. package/skills/agent-discord/SKILL.md +1 -1
  120. package/skills/agent-discordbot/SKILL.md +1 -1
  121. package/skills/agent-instagram/SKILL.md +1 -1
  122. package/skills/agent-kakaotalk/SKILL.md +12 -5
  123. package/skills/agent-line/SKILL.md +1 -1
  124. package/skills/agent-slack/SKILL.md +1 -1
  125. package/skills/agent-slackbot/SKILL.md +1 -1
  126. package/skills/agent-teams/SKILL.md +1 -1
  127. package/skills/agent-telegram/SKILL.md +1 -1
  128. package/skills/agent-telegrambot/SKILL.md +357 -0
  129. package/skills/agent-webex/SKILL.md +1 -1
  130. package/skills/agent-wechatbot/SKILL.md +1 -1
  131. package/skills/agent-whatsapp/SKILL.md +1 -1
  132. package/skills/agent-whatsappbot/SKILL.md +1 -1
  133. package/src/cli.ts +4 -0
  134. package/src/platforms/channeltalk/credential-manager.ts +2 -2
  135. package/src/platforms/channeltalkbot/credential-manager.ts +2 -2
  136. package/src/platforms/discord/credential-manager.ts +3 -2
  137. package/src/platforms/discordbot/credential-manager.ts +2 -2
  138. package/src/platforms/instagram/credential-manager.ts +2 -2
  139. package/src/platforms/kakaotalk/client.ts +3 -5
  140. package/src/platforms/kakaotalk/credential-manager.ts +2 -2
  141. package/src/platforms/line/client.ts +2 -2
  142. package/src/platforms/line/credential-manager.ts +2 -2
  143. package/src/platforms/slack/credential-manager.ts +2 -2
  144. package/src/platforms/slackbot/credential-manager.ts +2 -2
  145. package/src/platforms/teams/credential-manager.ts +2 -2
  146. package/src/platforms/telegram/commands/whoami.test.ts +1 -0
  147. package/src/platforms/telegram/credential-manager.ts +2 -2
  148. package/src/platforms/telegrambot/cli.ts +34 -0
  149. package/src/platforms/telegrambot/client.test.ts +454 -0
  150. package/src/platforms/telegrambot/client.ts +404 -0
  151. package/src/platforms/telegrambot/commands/auth.test.ts +244 -0
  152. package/src/platforms/telegrambot/commands/auth.ts +220 -0
  153. package/src/platforms/telegrambot/commands/chat.ts +96 -0
  154. package/src/platforms/telegrambot/commands/index.ts +5 -0
  155. package/src/platforms/telegrambot/commands/message.ts +235 -0
  156. package/src/platforms/telegrambot/commands/reaction.ts +70 -0
  157. package/src/platforms/telegrambot/commands/shared.ts +32 -0
  158. package/src/platforms/telegrambot/commands/whoami.ts +45 -0
  159. package/src/platforms/telegrambot/credential-manager.test.ts +196 -0
  160. package/src/platforms/telegrambot/credential-manager.ts +141 -0
  161. package/src/platforms/telegrambot/index.ts +44 -0
  162. package/src/platforms/telegrambot/listener.test.ts +398 -0
  163. package/src/platforms/telegrambot/listener.ts +198 -0
  164. package/src/platforms/telegrambot/types.test.ts +128 -0
  165. package/src/platforms/telegrambot/types.ts +282 -0
  166. package/src/platforms/webex/credential-manager.ts +2 -2
  167. package/src/platforms/wechatbot/credential-manager.ts +2 -2
  168. package/src/platforms/whatsapp/credential-manager.ts +2 -2
  169. package/src/platforms/whatsappbot/credential-manager.ts +2 -2
  170. package/src/shared/utils/config-dir.test.ts +41 -0
  171. package/src/shared/utils/config-dir.ts +23 -0
  172. package/src/shared/utils/derived-key-cache.ts +3 -2
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import {
7
7
  createAccountId,
8
8
  type TelegramAccount,
@@ -21,7 +21,7 @@ export class TelegramCredentialManager {
21
21
  private tdlibRootDir: string
22
22
 
23
23
  constructor(configDir?: string) {
24
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
24
+ this.configDir = configDir ?? getConfigDir()
25
25
  this.credentialsPath = join(this.configDir, 'telegram-credentials.json')
26
26
  this.provisioningStatePath = join(this.configDir, 'telegram-provisioning-state.json')
27
27
  this.tdlibRootDir = join(this.configDir, 'telegram')
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from 'commander'
4
+
5
+ import pkg from '../../../package.json' with { type: 'json' }
6
+ import { authCommand, chatCommand, messageCommand, reactionCommand, whoamiCommand } from './commands/index'
7
+
8
+ const program = new Command()
9
+
10
+ program
11
+ .name('agent-telegrambot')
12
+ .description('CLI tool for Telegram bot integration using bot tokens')
13
+ .version(pkg.version)
14
+ .option('--pretty', 'Pretty-print JSON output')
15
+ .option('--bot <id>', 'Bot ID to use')
16
+ .hook('preAction', (thisCmd, actionCmd) => {
17
+ for (const [key, value] of Object.entries(thisCmd.opts())) {
18
+ if (value === undefined) continue
19
+ const source = actionCmd.getOptionValueSource(key)
20
+ if (source === undefined || source === 'default') {
21
+ actionCmd.setOptionValue(key, value)
22
+ }
23
+ }
24
+ })
25
+
26
+ program.addCommand(authCommand)
27
+ program.addCommand(whoamiCommand)
28
+ program.addCommand(messageCommand)
29
+ program.addCommand(chatCommand)
30
+ program.addCommand(reactionCommand)
31
+
32
+ program.parseAsync(process.argv)
33
+
34
+ export default program
@@ -0,0 +1,454 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
+
3
+ import { TelegramBotClient } from './client'
4
+ import { TelegramBotError } from './types'
5
+
6
+ describe('TelegramBotClient', () => {
7
+ const originalFetch = globalThis.fetch
8
+ let fetchCalls: Array<{ url: string; options?: RequestInit }> = []
9
+ let fetchResponses: Response[] = []
10
+ let fetchIndex = 0
11
+
12
+ beforeEach(() => {
13
+ fetchCalls = []
14
+ fetchResponses = []
15
+ fetchIndex = 0
16
+ ;(globalThis as Record<string, unknown>).fetch = async (
17
+ url: string | URL | Request,
18
+ options?: RequestInit,
19
+ ): Promise<Response> => {
20
+ fetchCalls.push({ url: url.toString(), options })
21
+ const response = fetchResponses[fetchIndex]
22
+ fetchIndex++
23
+ if (!response) {
24
+ throw new Error('No mock response configured')
25
+ }
26
+ return response
27
+ }
28
+ })
29
+
30
+ afterEach(() => {
31
+ globalThis.fetch = originalFetch
32
+ })
33
+
34
+ const mockOk = (result: unknown): void => {
35
+ fetchResponses.push(
36
+ new Response(JSON.stringify({ ok: true, result }), {
37
+ status: 200,
38
+ headers: { 'Content-Type': 'application/json' },
39
+ }),
40
+ )
41
+ }
42
+
43
+ const mockApiError = (errorCode: number, description: string, params?: Record<string, unknown>): void => {
44
+ fetchResponses.push(
45
+ new Response(JSON.stringify({ ok: false, error_code: errorCode, description, parameters: params }), {
46
+ status: errorCode >= 400 && errorCode < 600 ? errorCode : 200,
47
+ headers: { 'Content-Type': 'application/json' },
48
+ }),
49
+ )
50
+ }
51
+
52
+ describe('login', () => {
53
+ it('requires non-empty token', async () => {
54
+ await expect(new TelegramBotClient().login({ token: '' })).rejects.toThrow(TelegramBotError)
55
+ await expect(new TelegramBotClient().login({ token: '' })).rejects.toThrow('Token is required')
56
+ })
57
+
58
+ it('accepts a valid token', async () => {
59
+ const client = await new TelegramBotClient().login({ token: '123:abc' })
60
+ expect(client).toBeInstanceOf(TelegramBotClient)
61
+ })
62
+ })
63
+
64
+ describe('getMe', () => {
65
+ it('returns bot user and uses /bot<token>/getMe URL', async () => {
66
+ mockOk({ id: 123456, is_bot: true, first_name: 'Test', username: 'testbot' })
67
+
68
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
69
+ const me = await client.getMe()
70
+
71
+ expect(me.id).toBe(123456)
72
+ expect(me.is_bot).toBe(true)
73
+ expect(me.username).toBe('testbot')
74
+ expect(fetchCalls[0].url).toBe('https://api.telegram.org/botTOKEN/getMe')
75
+ expect(fetchCalls[0].options?.method).toBe('POST')
76
+ })
77
+
78
+ it('throws TelegramBotError with code "unauthorized" on 401', async () => {
79
+ mockApiError(401, 'Unauthorized')
80
+ const client = await new TelegramBotClient().login({ token: 'bad' })
81
+ try {
82
+ await client.getMe()
83
+ expect(false).toBe(true)
84
+ } catch (e) {
85
+ expect(e).toBeInstanceOf(TelegramBotError)
86
+ expect((e as TelegramBotError).code).toBe('unauthorized')
87
+ }
88
+ })
89
+ })
90
+
91
+ describe('sendMessage', () => {
92
+ it('sends text message with chat_id and text body', async () => {
93
+ mockOk({
94
+ message_id: 42,
95
+ date: 1735689600,
96
+ chat: { id: -100123, type: 'supergroup', title: 'Eng' },
97
+ text: 'Hello',
98
+ })
99
+
100
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
101
+ const msg = await client.sendMessage(-100123, 'Hello')
102
+
103
+ expect(msg.message_id).toBe(42)
104
+ expect(msg.text).toBe('Hello')
105
+
106
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
107
+ expect(body.chat_id).toBe(-100123)
108
+ expect(body.text).toBe('Hello')
109
+ })
110
+
111
+ it('forwards optional parse_mode and reply_to_message_id', async () => {
112
+ mockOk({
113
+ message_id: 1,
114
+ date: 1,
115
+ chat: { id: 1, type: 'private', first_name: 'X' },
116
+ text: '<b>Hi</b>',
117
+ })
118
+
119
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
120
+ await client.sendMessage(1, '<b>Hi</b>', { parse_mode: 'HTML', reply_to_message_id: 5 })
121
+
122
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
123
+ expect(body.parse_mode).toBe('HTML')
124
+ expect(body.reply_to_message_id).toBe(5)
125
+ })
126
+ })
127
+
128
+ describe('rate limiting', () => {
129
+ it('retries after retry_after on 429', async () => {
130
+ mockApiError(429, 'Too Many Requests', { retry_after: 0 })
131
+ mockOk({ id: 1, is_bot: true, first_name: 'X' })
132
+
133
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
134
+ const me = await client.getMe()
135
+
136
+ expect(me.id).toBe(1)
137
+ expect(fetchCalls).toHaveLength(2)
138
+ })
139
+ })
140
+
141
+ describe('error mapping', () => {
142
+ it('maps 409 conflict to "conflict" code', async () => {
143
+ mockApiError(409, 'Conflict: terminated by other getUpdates request')
144
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
145
+ try {
146
+ await client.getMe()
147
+ expect(false).toBe(true)
148
+ } catch (e) {
149
+ expect(e).toBeInstanceOf(TelegramBotError)
150
+ expect((e as TelegramBotError).code).toBe('conflict')
151
+ }
152
+ })
153
+
154
+ it('maps 400 to "bad_request"', async () => {
155
+ mockApiError(400, 'Bad Request: chat not found')
156
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
157
+ try {
158
+ await client.getMe()
159
+ expect(false).toBe(true)
160
+ } catch (e) {
161
+ expect((e as TelegramBotError).code).toBe('bad_request')
162
+ }
163
+ })
164
+
165
+ it('maps 403 to "forbidden"', async () => {
166
+ mockApiError(403, "Forbidden: bot can't initiate conversation with a user")
167
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
168
+ try {
169
+ await client.getMe()
170
+ expect(false).toBe(true)
171
+ } catch (e) {
172
+ expect((e as TelegramBotError).code).toBe('forbidden')
173
+ }
174
+ })
175
+ })
176
+
177
+ describe('getUpdates', () => {
178
+ it('passes offset, limit, timeout to API', async () => {
179
+ mockOk([])
180
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
181
+ await client.getUpdates({ offset: 5, limit: 50, timeout: 30, allowed_updates: ['message'] })
182
+
183
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
184
+ expect(body.offset).toBe(5)
185
+ expect(body.limit).toBe(50)
186
+ expect(body.timeout).toBe(30)
187
+ expect(body.allowed_updates).toEqual(['message'])
188
+ })
189
+ })
190
+
191
+ describe('deleteMessage', () => {
192
+ it('returns boolean result', async () => {
193
+ mockOk(true)
194
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
195
+ const ok = await client.deleteMessage(123, 42)
196
+ expect(ok).toBe(true)
197
+ })
198
+ })
199
+
200
+ describe('setMessageReaction', () => {
201
+ it('sends reaction array', async () => {
202
+ mockOk(true)
203
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
204
+ await client.setMessageReaction(123, 42, [{ type: 'emoji', emoji: '👍' }], { is_big: true })
205
+
206
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
207
+ expect(body.reaction).toEqual([{ type: 'emoji', emoji: '👍' }])
208
+ expect(body.is_big).toBe(true)
209
+ })
210
+ })
211
+
212
+ describe('resolveChatId', () => {
213
+ it('returns numeric input as number', async () => {
214
+ const client = new TelegramBotClient()
215
+ expect(await client.resolveChatId(-100123)).toBe(-100123)
216
+ })
217
+
218
+ it('parses numeric string', async () => {
219
+ const client = new TelegramBotClient()
220
+ expect(await client.resolveChatId('-100123')).toBe(-100123)
221
+ })
222
+
223
+ it('keeps @username as is', async () => {
224
+ const client = new TelegramBotClient()
225
+ expect(await client.resolveChatId('@channel')).toBe('@channel')
226
+ })
227
+
228
+ it('prefixes plain username with @', async () => {
229
+ const client = new TelegramBotClient()
230
+ expect(await client.resolveChatId('channel')).toBe('@channel')
231
+ })
232
+ })
233
+
234
+ describe('formatChat', () => {
235
+ const client = new TelegramBotClient()
236
+
237
+ it('uses title when present', () => {
238
+ expect(client.formatChat({ id: 1, type: 'supergroup', title: 'Eng' })).toEqual({
239
+ id: 1,
240
+ type: 'supergroup',
241
+ name: 'Eng',
242
+ })
243
+ })
244
+
245
+ it('uses first_name + last_name when title missing', () => {
246
+ expect(client.formatChat({ id: 1, type: 'private', first_name: 'Ada', last_name: 'Lovelace' })).toEqual({
247
+ id: 1,
248
+ type: 'private',
249
+ name: 'Ada Lovelace',
250
+ })
251
+ })
252
+
253
+ it('falls back to username when name parts are empty', () => {
254
+ expect(client.formatChat({ id: 1, type: 'private', username: 'ada' })).toEqual({
255
+ id: 1,
256
+ type: 'private',
257
+ name: 'ada',
258
+ })
259
+ })
260
+
261
+ it('falls back to id when nothing is set', () => {
262
+ expect(client.formatChat({ id: 42, type: 'private' })).toEqual({ id: 42, type: 'private', name: '42' })
263
+ })
264
+ })
265
+
266
+ describe('getChat / getChatMember / getChatMemberCount', () => {
267
+ it('getChat sends chat_id', async () => {
268
+ mockOk({ id: -100123, type: 'supergroup', title: 'Eng', description: 'ENG team' })
269
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
270
+ const chat = await client.getChat(-100123)
271
+
272
+ expect(chat.id).toBe(-100123)
273
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
274
+ expect(body.chat_id).toBe(-100123)
275
+ })
276
+
277
+ it('getChatMember sends chat_id and user_id', async () => {
278
+ mockOk({ user: { id: 1, is_bot: false, first_name: 'A' }, status: 'member' })
279
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
280
+ await client.getChatMember(-100123, 42)
281
+
282
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
283
+ expect(body.chat_id).toBe(-100123)
284
+ expect(body.user_id).toBe(42)
285
+ })
286
+
287
+ it('getChatMemberCount returns number', async () => {
288
+ mockOk(7)
289
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
290
+ const count = await client.getChatMemberCount(-100123)
291
+ expect(count).toBe(7)
292
+ })
293
+ })
294
+
295
+ describe('forwardMessage', () => {
296
+ it('sends chat_id, from_chat_id, message_id', async () => {
297
+ mockOk({
298
+ message_id: 99,
299
+ date: 1,
300
+ chat: { id: 200, type: 'supergroup', title: 'Dst' },
301
+ text: 'forwarded',
302
+ })
303
+
304
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
305
+ const msg = await client.forwardMessage(200, 100, 42)
306
+
307
+ expect(msg.message_id).toBe(99)
308
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
309
+ expect(body.chat_id).toBe(200)
310
+ expect(body.from_chat_id).toBe(100)
311
+ expect(body.message_id).toBe(42)
312
+ })
313
+ })
314
+
315
+ describe('editMessageText', () => {
316
+ it('accepts chat-message target', async () => {
317
+ mockOk({
318
+ message_id: 42,
319
+ date: 1,
320
+ chat: { id: 1, type: 'private', first_name: 'A' },
321
+ text: 'edited',
322
+ })
323
+
324
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
325
+ const result = await client.editMessageText({ chat_id: 1, message_id: 42 }, 'edited')
326
+
327
+ expect(typeof result === 'object' && result !== null && 'message_id' in result).toBe(true)
328
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
329
+ expect(body.chat_id).toBe(1)
330
+ expect(body.message_id).toBe(42)
331
+ expect(body.text).toBe('edited')
332
+ })
333
+
334
+ it('accepts inline-message target', async () => {
335
+ mockOk(true)
336
+
337
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
338
+ const result = await client.editMessageText({ inline_message_id: 'inline123' }, 'edited')
339
+
340
+ expect(result).toBe(true)
341
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
342
+ expect(body.inline_message_id).toBe('inline123')
343
+ expect(body.chat_id).toBeUndefined()
344
+ })
345
+ })
346
+
347
+ describe('sendDocument (multipart)', () => {
348
+ it('uploads file with multipart/form-data and forwards optional caption', async () => {
349
+ const fs = await import('node:fs/promises')
350
+ const os = await import('node:os')
351
+ const path = await import('node:path')
352
+ const tmp = path.join(os.tmpdir(), `tgbot-doc-test-${Date.now()}.txt`)
353
+ await fs.writeFile(tmp, 'hello')
354
+
355
+ try {
356
+ mockOk({
357
+ message_id: 1,
358
+ date: 1,
359
+ chat: { id: 1, type: 'private', first_name: 'A' },
360
+ document: { file_id: 'F1', file_unique_id: 'U1', file_name: tmp.split('/').pop() },
361
+ })
362
+
363
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
364
+ const msg = await client.sendDocument(1, tmp, { caption: 'hi' })
365
+
366
+ expect(msg.message_id).toBe(1)
367
+ const body = fetchCalls[0].options?.body
368
+ expect(body).toBeInstanceOf(FormData)
369
+ const fd = body as unknown as FormData
370
+ expect(fd.get('chat_id')).toBe('1')
371
+ expect(fd.get('caption')).toBe('hi')
372
+ expect(fd.get('document')).toBeInstanceOf(Blob)
373
+ } finally {
374
+ await fs.unlink(tmp).catch(() => undefined)
375
+ }
376
+ })
377
+ })
378
+
379
+ describe('setWebhook / deleteWebhook', () => {
380
+ it('deleteWebhook with drop_pending_updates', async () => {
381
+ mockOk(true)
382
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
383
+ const ok = await client.deleteWebhook({ drop_pending_updates: true })
384
+
385
+ expect(ok).toBe(true)
386
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
387
+ expect(body.drop_pending_updates).toBe(true)
388
+ })
389
+
390
+ it('setWebhook with url', async () => {
391
+ mockOk(true)
392
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
393
+ await client.setWebhook('https://example.com/hook', { secret_token: 's3cret' })
394
+
395
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
396
+ expect(body.url).toBe('https://example.com/hook')
397
+ expect(body.secret_token).toBe('s3cret')
398
+ })
399
+ })
400
+
401
+ describe('abort', () => {
402
+ it('throws AbortError when signal is already aborted', async () => {
403
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
404
+ const ac = new AbortController()
405
+ ac.abort()
406
+ try {
407
+ await client.getUpdates(undefined, ac.signal)
408
+ expect(false).toBe(true)
409
+ } catch (e) {
410
+ expect((e as Error).name).toBe('AbortError')
411
+ }
412
+ })
413
+
414
+ it('treats signal.aborted as authoritative even if fetch throws non-AbortError', async () => {
415
+ ;(globalThis as Record<string, unknown>).fetch = async (
416
+ _url: string | URL | Request,
417
+ init?: RequestInit,
418
+ ): Promise<Response> => {
419
+ const signal = init?.signal as AbortSignal | undefined
420
+ if (signal?.aborted) {
421
+ throw new Error('connection reset')
422
+ }
423
+ return new Response(JSON.stringify({ ok: true, result: [] }), { status: 200 })
424
+ }
425
+
426
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
427
+ const ac = new AbortController()
428
+ ac.abort()
429
+ try {
430
+ await client.getUpdates(undefined, ac.signal)
431
+ expect(false).toBe(true)
432
+ } catch (e) {
433
+ expect((e as Error).name).toBe('AbortError')
434
+ }
435
+ })
436
+ })
437
+
438
+ describe('5xx retry', () => {
439
+ it('retries 502 with non-JSON body and succeeds on next attempt', async () => {
440
+ fetchResponses.push(
441
+ new Response('<html>502 Bad Gateway</html>', {
442
+ status: 502,
443
+ headers: { 'Content-Type': 'text/html' },
444
+ }),
445
+ )
446
+ mockOk({ id: 1, is_bot: true, first_name: 'X' })
447
+
448
+ const client = await new TelegramBotClient().login({ token: 'TOKEN' })
449
+ const me = await client.getMe()
450
+ expect(me.id).toBe(1)
451
+ expect(fetchCalls).toHaveLength(2)
452
+ })
453
+ })
454
+ })