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
@@ -0,0 +1,404 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ import type {
4
+ TelegramBotUser,
5
+ TelegramChat,
6
+ TelegramChatFullInfo,
7
+ TelegramChatMember,
8
+ TelegramMessage,
9
+ TelegramReactionType,
10
+ TelegramUpdate,
11
+ } from './types'
12
+ import { TelegramBotError } from './types'
13
+
14
+ const BASE_URL = 'https://api.telegram.org'
15
+ const MAX_RETRIES = 3
16
+ const BASE_BACKOFF_MS = 100
17
+
18
+ interface ApiResponse<T> {
19
+ ok: boolean
20
+ result?: T
21
+ description?: string
22
+ error_code?: number
23
+ parameters?: {
24
+ retry_after?: number
25
+ migrate_to_chat_id?: number
26
+ }
27
+ }
28
+
29
+ export interface SendMessageOptions {
30
+ parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2'
31
+ disable_web_page_preview?: boolean
32
+ disable_notification?: boolean
33
+ protect_content?: boolean
34
+ reply_to_message_id?: number
35
+ message_thread_id?: number
36
+ }
37
+
38
+ export interface GetUpdatesOptions {
39
+ offset?: number
40
+ limit?: number
41
+ timeout?: number
42
+ allowed_updates?: string[]
43
+ }
44
+
45
+ export interface EditMessageTextChat {
46
+ chat_id: number | string
47
+ message_id: number
48
+ inline_message_id?: never
49
+ }
50
+
51
+ export interface EditMessageTextInline {
52
+ inline_message_id: string
53
+ chat_id?: never
54
+ message_id?: never
55
+ }
56
+
57
+ export type EditMessageTextTarget = EditMessageTextChat | EditMessageTextInline
58
+
59
+ export type BotReactionType = { type: 'emoji'; emoji: string }
60
+
61
+ export type ChatId = number | string
62
+
63
+ export class TelegramBotClient {
64
+ private token: string | null = null
65
+
66
+ async login(credentials?: { token: string }): Promise<this> {
67
+ if (credentials) {
68
+ if (!credentials.token) {
69
+ throw new TelegramBotError('Token is required', 'missing_token')
70
+ }
71
+ this.token = credentials.token
72
+ return this
73
+ }
74
+
75
+ const { TelegramBotCredentialManager } = await import('./credential-manager')
76
+ const credManager = new TelegramBotCredentialManager()
77
+ const creds = await credManager.getCredentials()
78
+ if (!creds?.token) {
79
+ throw new TelegramBotError('No Telegram bot credentials found. Run "auth set <token>" first.', 'no_credentials')
80
+ }
81
+ return this.login({ token: creds.token })
82
+ }
83
+
84
+ private ensureAuth(): string {
85
+ if (!this.token) {
86
+ throw new TelegramBotError('Not authenticated. Call .login() first.', 'not_authenticated')
87
+ }
88
+ return this.token
89
+ }
90
+
91
+ private buildUrl(method: string): string {
92
+ return `${BASE_URL}/bot${this.ensureAuth()}/${method}`
93
+ }
94
+
95
+ private sleep(ms: number): Promise<void> {
96
+ return new Promise((resolve) => setTimeout(resolve, ms))
97
+ }
98
+
99
+ private isAbort(signal: AbortSignal | undefined, error: unknown): boolean {
100
+ if (signal?.aborted) return true
101
+ const name = (error as { name?: string } | null)?.name
102
+ return name === 'AbortError'
103
+ }
104
+
105
+ private throwApiError(method: string, body: ApiResponse<unknown>): never {
106
+ const code = body.error_code
107
+ const description = body.description ?? `HTTP error from ${method}`
108
+ if (code === 401) {
109
+ throw new TelegramBotError(`Unauthorized: ${description}`, 'unauthorized')
110
+ }
111
+ if (code === 409) {
112
+ throw new TelegramBotError(`Conflict: ${description}`, 'conflict')
113
+ }
114
+ if (code === 403) {
115
+ throw new TelegramBotError(`Forbidden: ${description}`, 'forbidden')
116
+ }
117
+ if (code === 400) {
118
+ throw new TelegramBotError(`Bad Request: ${description}`, 'bad_request')
119
+ }
120
+ if (code === 404) {
121
+ throw new TelegramBotError(`Not Found: ${description}`, 'not_found')
122
+ }
123
+ throw new TelegramBotError(description, code !== undefined ? `http_${code}` : 'http_error')
124
+ }
125
+
126
+ private async parseJsonOrRetry<T>(
127
+ response: Response,
128
+ method: string,
129
+ attempt: number,
130
+ ): Promise<{ body: ApiResponse<T> } | { retry: true }> {
131
+ try {
132
+ const body = (await response.json()) as ApiResponse<T>
133
+ return { body }
134
+ } catch {
135
+ // 5xx responses sometimes return HTML/empty bodies; allow retry instead of failing fast.
136
+ if (response.status >= 500 && attempt < MAX_RETRIES) {
137
+ return { retry: true }
138
+ }
139
+ throw new TelegramBotError(`Invalid JSON response from ${method} (HTTP ${response.status})`, 'invalid_response')
140
+ }
141
+ }
142
+
143
+ private async call<T>(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<T> {
144
+ return this.requestJson<T>(method, { method: 'POST', body: params }, signal)
145
+ }
146
+
147
+ private async requestJson<T>(
148
+ method: string,
149
+ request: { method: 'POST' | 'GET'; body?: Record<string, unknown> },
150
+ signal?: AbortSignal,
151
+ ): Promise<T> {
152
+ const url = this.buildUrl(method)
153
+ let lastError: Error | undefined
154
+
155
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
156
+ if (signal?.aborted) {
157
+ throw Object.assign(new Error('Aborted'), { name: 'AbortError' })
158
+ }
159
+
160
+ const init: RequestInit = {
161
+ method: request.method,
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: request.body ? JSON.stringify(request.body) : undefined,
164
+ signal,
165
+ }
166
+
167
+ let response: Response
168
+ try {
169
+ response = await fetch(url, init)
170
+ } catch (error) {
171
+ if (this.isAbort(signal, error)) {
172
+ throw error instanceof Error ? error : Object.assign(new Error('Aborted'), { name: 'AbortError' })
173
+ }
174
+ lastError = error instanceof Error ? error : new Error(String(error))
175
+ if (attempt < MAX_RETRIES) {
176
+ await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
177
+ continue
178
+ }
179
+ throw new TelegramBotError(`Network error: ${lastError.message}`, 'network_error')
180
+ }
181
+
182
+ const parsed = await this.parseJsonOrRetry<T>(response, method, attempt)
183
+ if ('retry' in parsed) {
184
+ await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
185
+ continue
186
+ }
187
+ const { body } = parsed
188
+
189
+ if (response.status === 429 || body.error_code === 429) {
190
+ const retryAfter = body.parameters?.retry_after ?? 1
191
+ if (attempt < MAX_RETRIES) {
192
+ await this.sleep(retryAfter * 1000)
193
+ continue
194
+ }
195
+ throw new TelegramBotError(body.description ?? 'Rate limited', 'rate_limited')
196
+ }
197
+
198
+ if (response.status >= 500 && attempt < MAX_RETRIES) {
199
+ await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
200
+ continue
201
+ }
202
+
203
+ if (!body.ok) {
204
+ this.throwApiError(method, body)
205
+ }
206
+
207
+ if (body.result === undefined) {
208
+ return undefined as T
209
+ }
210
+ return body.result
211
+ }
212
+
213
+ throw lastError ?? new TelegramBotError('Request failed after retries', 'max_retries')
214
+ }
215
+
216
+ private async callMultipart<T>(method: string, formData: FormData, signal?: AbortSignal): Promise<T> {
217
+ const url = this.buildUrl(method)
218
+ let lastError: Error | undefined
219
+
220
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
221
+ if (signal?.aborted) {
222
+ throw Object.assign(new Error('Aborted'), { name: 'AbortError' })
223
+ }
224
+
225
+ let response: Response
226
+ try {
227
+ response = await fetch(url, { method: 'POST', body: formData, signal })
228
+ } catch (error) {
229
+ if (this.isAbort(signal, error)) {
230
+ throw error instanceof Error ? error : Object.assign(new Error('Aborted'), { name: 'AbortError' })
231
+ }
232
+ lastError = error instanceof Error ? error : new Error(String(error))
233
+ if (attempt < MAX_RETRIES) {
234
+ await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
235
+ continue
236
+ }
237
+ throw new TelegramBotError(`Network error: ${lastError.message}`, 'network_error')
238
+ }
239
+
240
+ const parsed = await this.parseJsonOrRetry<T>(response, method, attempt)
241
+ if ('retry' in parsed) {
242
+ await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
243
+ continue
244
+ }
245
+ const { body } = parsed
246
+
247
+ if (response.status === 429 || body.error_code === 429) {
248
+ const retryAfter = body.parameters?.retry_after ?? 1
249
+ if (attempt < MAX_RETRIES) {
250
+ await this.sleep(retryAfter * 1000)
251
+ continue
252
+ }
253
+ throw new TelegramBotError(body.description ?? 'Rate limited', 'rate_limited')
254
+ }
255
+
256
+ if (response.status >= 500 && attempt < MAX_RETRIES) {
257
+ await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
258
+ continue
259
+ }
260
+
261
+ if (!body.ok) {
262
+ this.throwApiError(method, body)
263
+ }
264
+
265
+ if (body.result === undefined) {
266
+ return undefined as T
267
+ }
268
+ return body.result
269
+ }
270
+
271
+ throw lastError ?? new TelegramBotError('Request failed after retries', 'max_retries')
272
+ }
273
+
274
+ async getMe(): Promise<TelegramBotUser> {
275
+ return this.call<TelegramBotUser>('getMe')
276
+ }
277
+
278
+ async getChat(chatId: ChatId): Promise<TelegramChatFullInfo> {
279
+ return this.call<TelegramChatFullInfo>('getChat', { chat_id: chatId })
280
+ }
281
+
282
+ async getChatMember(chatId: ChatId, userId: number): Promise<TelegramChatMember> {
283
+ return this.call<TelegramChatMember>('getChatMember', { chat_id: chatId, user_id: userId })
284
+ }
285
+
286
+ async getChatMemberCount(chatId: ChatId): Promise<number> {
287
+ return this.call<number>('getChatMemberCount', { chat_id: chatId })
288
+ }
289
+
290
+ async sendMessage(chatId: ChatId, text: string, options?: SendMessageOptions): Promise<TelegramMessage> {
291
+ return this.call<TelegramMessage>('sendMessage', { chat_id: chatId, text, ...options })
292
+ }
293
+
294
+ async sendPhoto(
295
+ chatId: ChatId,
296
+ photo: string,
297
+ options?: { caption?: string; parse_mode?: SendMessageOptions['parse_mode'] },
298
+ ): Promise<TelegramMessage> {
299
+ return this.call<TelegramMessage>('sendPhoto', { chat_id: chatId, photo, ...options })
300
+ }
301
+
302
+ async sendDocument(
303
+ chatId: ChatId,
304
+ filePath: string,
305
+ options?: { caption?: string; parse_mode?: SendMessageOptions['parse_mode']; signal?: AbortSignal },
306
+ ): Promise<TelegramMessage> {
307
+ const fileBuffer = await readFile(filePath)
308
+ const filename = filePath.split('/').pop() || 'file'
309
+
310
+ const formData = new FormData()
311
+ formData.append('chat_id', String(chatId))
312
+ formData.append('document', new Blob([new Uint8Array(fileBuffer)]), filename)
313
+ if (options?.caption !== undefined) formData.append('caption', options.caption)
314
+ if (options?.parse_mode !== undefined) formData.append('parse_mode', options.parse_mode)
315
+
316
+ return this.callMultipart<TelegramMessage>('sendDocument', formData, options?.signal)
317
+ }
318
+
319
+ async forwardMessage(
320
+ chatId: ChatId,
321
+ fromChatId: ChatId,
322
+ messageId: number,
323
+ options?: { disable_notification?: boolean; protect_content?: boolean; message_thread_id?: number },
324
+ ): Promise<TelegramMessage> {
325
+ return this.call<TelegramMessage>('forwardMessage', {
326
+ chat_id: chatId,
327
+ from_chat_id: fromChatId,
328
+ message_id: messageId,
329
+ ...options,
330
+ })
331
+ }
332
+
333
+ async editMessageText(
334
+ target: EditMessageTextTarget,
335
+ text: string,
336
+ options?: SendMessageOptions,
337
+ ): Promise<TelegramMessage | true> {
338
+ return this.call<TelegramMessage | true>('editMessageText', { ...target, text, ...options })
339
+ }
340
+
341
+ async deleteMessage(chatId: ChatId, messageId: number): Promise<boolean> {
342
+ return this.call<boolean>('deleteMessage', { chat_id: chatId, message_id: messageId })
343
+ }
344
+
345
+ async setMessageReaction(
346
+ chatId: ChatId,
347
+ messageId: number,
348
+ reaction: BotReactionType[],
349
+ options?: { is_big?: boolean },
350
+ ): Promise<boolean> {
351
+ return this.call<boolean>('setMessageReaction', {
352
+ chat_id: chatId,
353
+ message_id: messageId,
354
+ reaction,
355
+ ...options,
356
+ })
357
+ }
358
+
359
+ async setMessageReactionRaw(
360
+ chatId: ChatId,
361
+ messageId: number,
362
+ reaction: TelegramReactionType[],
363
+ options?: { is_big?: boolean },
364
+ ): Promise<boolean> {
365
+ return this.call<boolean>('setMessageReaction', {
366
+ chat_id: chatId,
367
+ message_id: messageId,
368
+ reaction,
369
+ ...options,
370
+ })
371
+ }
372
+
373
+ async getUpdates(options?: GetUpdatesOptions, signal?: AbortSignal): Promise<TelegramUpdate[]> {
374
+ const params: Record<string, unknown> = {}
375
+ if (options?.offset !== undefined) params.offset = options.offset
376
+ if (options?.limit !== undefined) params.limit = options.limit
377
+ if (options?.timeout !== undefined) params.timeout = options.timeout
378
+ if (options?.allowed_updates !== undefined) params.allowed_updates = options.allowed_updates
379
+ return this.call<TelegramUpdate[]>('getUpdates', params, signal)
380
+ }
381
+
382
+ async deleteWebhook(options?: { drop_pending_updates?: boolean }): Promise<boolean> {
383
+ return this.call<boolean>('deleteWebhook', options)
384
+ }
385
+
386
+ async setWebhook(url: string, options?: Record<string, unknown>): Promise<boolean> {
387
+ return this.call<boolean>('setWebhook', { url, ...options })
388
+ }
389
+
390
+ async resolveChatId(chat: ChatId): Promise<ChatId> {
391
+ if (typeof chat === 'number') return chat
392
+ if (/^-?\d+$/.test(chat)) return Number(chat)
393
+ if (chat.startsWith('@')) return chat
394
+ return `@${chat}`
395
+ }
396
+
397
+ formatChat(chat: TelegramChat): { id: number; type: string; name: string } {
398
+ if (chat.title) return { id: chat.id, type: chat.type, name: chat.title }
399
+ const fullName = [chat.first_name, chat.last_name].filter(Boolean).join(' ')
400
+ if (fullName) return { id: chat.id, type: chat.type, name: fullName }
401
+ if (chat.username) return { id: chat.id, type: chat.type, name: chat.username }
402
+ return { id: chat.id, type: chat.type, name: String(chat.id) }
403
+ }
404
+ }
@@ -0,0 +1,244 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
+ import { existsSync, rmSync } from 'node:fs'
3
+ import { mkdir } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import type { TelegramBotClient } from '../client'
8
+ import { TelegramBotCredentialManager } from '../credential-manager'
9
+ import type { TelegramBotUser } from '../types'
10
+ import { clearAction, listAction, removeAction, setAction, statusAction, useAction } from './auth'
11
+
12
+ interface MockOptions {
13
+ user?: TelegramBotUser
14
+ loginError?: Error
15
+ getMeError?: Error
16
+ }
17
+
18
+ function createMockClient(options: MockOptions = {}): TelegramBotClient {
19
+ const user = options.user ?? {
20
+ id: 123456,
21
+ is_bot: true,
22
+ first_name: 'Test',
23
+ username: 'testbot',
24
+ }
25
+ return {
26
+ async login(): Promise<TelegramBotClient> {
27
+ if (options.loginError) throw options.loginError
28
+ return this as unknown as TelegramBotClient
29
+ },
30
+ async getMe(): Promise<TelegramBotUser> {
31
+ if (options.getMeError) throw options.getMeError
32
+ return user
33
+ },
34
+ } as unknown as TelegramBotClient
35
+ }
36
+
37
+ describe('auth commands', () => {
38
+ let tempDir: string
39
+ let originalEnv: NodeJS.ProcessEnv
40
+
41
+ beforeEach(async () => {
42
+ tempDir = join(tmpdir(), `telegrambot-auth-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
43
+ await mkdir(tempDir, { recursive: true })
44
+ originalEnv = { ...process.env }
45
+ delete process.env.E2E_TELEGRAMBOT_TOKEN
46
+ })
47
+
48
+ afterEach(() => {
49
+ if (existsSync(tempDir)) {
50
+ rmSync(tempDir, { recursive: true })
51
+ }
52
+ process.env = originalEnv
53
+ })
54
+
55
+ describe('setAction', () => {
56
+ it('validates and stores bot token using username as bot_id', async () => {
57
+ const manager = new TelegramBotCredentialManager(tempDir)
58
+
59
+ const result = await setAction('123:abc', {
60
+ _credManager: manager,
61
+ _clientFactory: () => createMockClient(),
62
+ })
63
+
64
+ expect(result.success).toBe(true)
65
+ expect(result.bot_id).toBe('testbot')
66
+ expect(result.bot_name).toBe('testbot')
67
+
68
+ const creds = await manager.getCredentials()
69
+ expect(creds?.token).toBe('123:abc')
70
+ expect(creds?.bot_id).toBe('testbot')
71
+ })
72
+
73
+ it('uses --bot flag as bot_id', async () => {
74
+ const manager = new TelegramBotCredentialManager(tempDir)
75
+
76
+ const result = await setAction('123:abc', {
77
+ bot: 'deploy',
78
+ _credManager: manager,
79
+ _clientFactory: () => createMockClient(),
80
+ })
81
+
82
+ expect(result.bot_id).toBe('deploy')
83
+ expect(await manager.getCredentials('deploy')).not.toBeNull()
84
+ })
85
+
86
+ it('falls back to numeric id when username missing', async () => {
87
+ const manager = new TelegramBotCredentialManager(tempDir)
88
+ const result = await setAction('123:abc', {
89
+ _credManager: manager,
90
+ _clientFactory: () =>
91
+ createMockClient({
92
+ user: { id: 123456, is_bot: true, first_name: 'NoUsername' },
93
+ }),
94
+ })
95
+
96
+ expect(result.bot_id).toBe('123456')
97
+ })
98
+
99
+ it('rejects non-bot tokens (is_bot: false)', async () => {
100
+ const manager = new TelegramBotCredentialManager(tempDir)
101
+ const result = await setAction('user-token', {
102
+ _credManager: manager,
103
+ _clientFactory: () =>
104
+ createMockClient({
105
+ user: { id: 123456, is_bot: false, first_name: 'User' },
106
+ }),
107
+ })
108
+
109
+ expect(result.error).toBeDefined()
110
+ expect(result.error).toContain('not')
111
+ })
112
+
113
+ it('handles client errors', async () => {
114
+ const manager = new TelegramBotCredentialManager(tempDir)
115
+ const result = await setAction('bad', {
116
+ _credManager: manager,
117
+ _clientFactory: () =>
118
+ createMockClient({
119
+ getMeError: new Error('Unauthorized: invalid token'),
120
+ }),
121
+ })
122
+
123
+ expect(result.error).toContain('Unauthorized')
124
+ })
125
+
126
+ it('returns error when login itself fails', async () => {
127
+ const manager = new TelegramBotCredentialManager(tempDir)
128
+ const result = await setAction('bad', {
129
+ _credManager: manager,
130
+ _clientFactory: () =>
131
+ createMockClient({
132
+ loginError: new Error('Token is required'),
133
+ }),
134
+ })
135
+
136
+ expect(result.error).toBe('Token is required')
137
+ expect(await manager.getCredentials()).toBeNull()
138
+ })
139
+ })
140
+
141
+ describe('clearAction', () => {
142
+ it('removes all stored credentials', async () => {
143
+ const manager = new TelegramBotCredentialManager(tempDir)
144
+ await manager.setCredentials({ token: 't', bot_id: 'b', bot_name: 'B' })
145
+
146
+ const result = await clearAction({ _credManager: manager })
147
+
148
+ expect(result.success).toBe(true)
149
+ expect(await manager.getCredentials()).toBeNull()
150
+ })
151
+ })
152
+
153
+ describe('statusAction', () => {
154
+ it('returns no credentials when none set', async () => {
155
+ const manager = new TelegramBotCredentialManager(tempDir)
156
+ const result = await statusAction({ _credManager: manager })
157
+
158
+ expect(result.valid).toBe(false)
159
+ expect(result.error).toBeDefined()
160
+ })
161
+
162
+ it('returns valid status for current bot', async () => {
163
+ const manager = new TelegramBotCredentialManager(tempDir)
164
+ await manager.setCredentials({ token: '123:abc', bot_id: 'mybot', bot_name: 'My Bot' })
165
+
166
+ const result = await statusAction({
167
+ _credManager: manager,
168
+ _clientFactory: () => createMockClient(),
169
+ })
170
+
171
+ expect(result.valid).toBe(true)
172
+ expect(result.bot_id).toBe('mybot')
173
+ })
174
+
175
+ it('returns invalid when getMe fails', async () => {
176
+ const manager = new TelegramBotCredentialManager(tempDir)
177
+ await manager.setCredentials({ token: 'bad', bot_id: 'mybot', bot_name: 'My Bot' })
178
+
179
+ const result = await statusAction({
180
+ _credManager: manager,
181
+ _clientFactory: () =>
182
+ createMockClient({
183
+ getMeError: new Error('401'),
184
+ }),
185
+ })
186
+ expect(result.valid).toBe(false)
187
+ })
188
+ })
189
+
190
+ describe('listAction', () => {
191
+ it('returns empty list when no bots', async () => {
192
+ const manager = new TelegramBotCredentialManager(tempDir)
193
+ const result = await listAction({ _credManager: manager })
194
+ expect(result.bots).toEqual([])
195
+ })
196
+
197
+ it('returns all bots with current flag', async () => {
198
+ const manager = new TelegramBotCredentialManager(tempDir)
199
+ await manager.setCredentials({ token: 't1', bot_id: 'a', bot_name: 'A' })
200
+ await manager.setCredentials({ token: 't2', bot_id: 'b', bot_name: 'B' })
201
+
202
+ const result = await listAction({ _credManager: manager })
203
+ expect(result.bots).toHaveLength(2)
204
+ expect(result.bots?.find((x) => x.bot_id === 'b')?.is_current).toBe(true)
205
+ })
206
+ })
207
+
208
+ describe('useAction', () => {
209
+ it('switches active bot', async () => {
210
+ const manager = new TelegramBotCredentialManager(tempDir)
211
+ await manager.setCredentials({ token: 't1', bot_id: 'a', bot_name: 'A' })
212
+ await manager.setCredentials({ token: 't2', bot_id: 'b', bot_name: 'B' })
213
+
214
+ const result = await useAction('a', { _credManager: manager })
215
+
216
+ expect(result.success).toBe(true)
217
+ expect(result.bot_id).toBe('a')
218
+ })
219
+
220
+ it('returns error for unknown bot', async () => {
221
+ const manager = new TelegramBotCredentialManager(tempDir)
222
+ const result = await useAction('nope', { _credManager: manager })
223
+ expect(result.error).toContain('not found')
224
+ })
225
+ })
226
+
227
+ describe('removeAction', () => {
228
+ it('removes a stored bot', async () => {
229
+ const manager = new TelegramBotCredentialManager(tempDir)
230
+ await manager.setCredentials({ token: 't', bot_id: 'a', bot_name: 'A' })
231
+
232
+ const result = await removeAction('a', { _credManager: manager })
233
+
234
+ expect(result.success).toBe(true)
235
+ expect(await manager.getCredentials('a')).toBeNull()
236
+ })
237
+
238
+ it('returns error for unknown bot', async () => {
239
+ const manager = new TelegramBotCredentialManager(tempDir)
240
+ const result = await removeAction('nope', { _credManager: manager })
241
+ expect(result.error).toContain('not found')
242
+ })
243
+ })
244
+ })