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.
- package/.claude-plugin/README.md +11 -1
- package/.claude-plugin/marketplace.json +14 -1
- package/.claude-plugin/plugin.json +4 -2
- package/CONTRIBUTING.md +12 -0
- package/README.md +41 -4
- package/dist/package.json +10 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/channeltalk/credential-manager.js +2 -2
- package/dist/src/platforms/channeltalk/credential-manager.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/credential-manager.js +2 -2
- package/dist/src/platforms/channeltalkbot/credential-manager.js.map +1 -1
- package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/discord/credential-manager.js +2 -2
- package/dist/src/platforms/discord/credential-manager.js.map +1 -1
- package/dist/src/platforms/discordbot/credential-manager.js +2 -2
- package/dist/src/platforms/discordbot/credential-manager.js.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.js +2 -2
- package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +3 -4
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/credential-manager.js +2 -2
- package/dist/src/platforms/kakaotalk/credential-manager.js.map +1 -1
- package/dist/src/platforms/line/client.js +2 -2
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/line/credential-manager.js +2 -2
- package/dist/src/platforms/line/credential-manager.js.map +1 -1
- package/dist/src/platforms/slack/credential-manager.js +2 -2
- package/dist/src/platforms/slack/credential-manager.js.map +1 -1
- package/dist/src/platforms/slackbot/credential-manager.js +2 -2
- package/dist/src/platforms/slackbot/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +2 -2
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/telegram/credential-manager.js +2 -2
- package/dist/src/platforms/telegram/credential-manager.js.map +1 -1
- package/dist/src/platforms/telegrambot/cli.d.ts +5 -0
- package/dist/src/platforms/telegrambot/cli.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/cli.js +29 -0
- package/dist/src/platforms/telegrambot/cli.js.map +1 -0
- package/dist/src/platforms/telegrambot/client.d.ts +85 -0
- package/dist/src/platforms/telegrambot/client.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/client.js +282 -0
- package/dist/src/platforms/telegrambot/client.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/auth.d.ts +31 -0
- package/dist/src/platforms/telegrambot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/auth.js +173 -0
- package/dist/src/platforms/telegrambot/commands/auth.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/chat.d.ts +25 -0
- package/dist/src/platforms/telegrambot/commands/chat.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/chat.js +69 -0
- package/dist/src/platforms/telegrambot/commands/chat.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/index.d.ts +6 -0
- package/dist/src/platforms/telegrambot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/index.js +6 -0
- package/dist/src/platforms/telegrambot/commands/index.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/message.d.ts +39 -0
- package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/message.js +145 -0
- package/dist/src/platforms/telegrambot/commands/message.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/reaction.d.ts +16 -0
- package/dist/src/platforms/telegrambot/commands/reaction.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/reaction.js +49 -0
- package/dist/src/platforms/telegrambot/commands/reaction.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/shared.d.ts +12 -0
- package/dist/src/platforms/telegrambot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/shared.js +21 -0
- package/dist/src/platforms/telegrambot/commands/shared.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/whoami.d.ts +17 -0
- package/dist/src/platforms/telegrambot/commands/whoami.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/whoami.js +30 -0
- package/dist/src/platforms/telegrambot/commands/whoami.js.map +1 -0
- package/dist/src/platforms/telegrambot/credential-manager.d.ts +17 -0
- package/dist/src/platforms/telegrambot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/credential-manager.js +113 -0
- package/dist/src/platforms/telegrambot/credential-manager.js.map +1 -0
- package/dist/src/platforms/telegrambot/index.d.ts +7 -0
- package/dist/src/platforms/telegrambot/index.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/index.js +5 -0
- package/dist/src/platforms/telegrambot/index.js.map +1 -0
- package/dist/src/platforms/telegrambot/listener.d.ts +30 -0
- package/dist/src/platforms/telegrambot/listener.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/listener.js +186 -0
- package/dist/src/platforms/telegrambot/listener.js.map +1 -0
- package/dist/src/platforms/telegrambot/types.d.ts +256 -0
- package/dist/src/platforms/telegrambot/types.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/types.js +96 -0
- package/dist/src/platforms/telegrambot/types.js.map +1 -0
- package/dist/src/platforms/webex/credential-manager.js +2 -2
- package/dist/src/platforms/webex/credential-manager.js.map +1 -1
- package/dist/src/platforms/wechatbot/credential-manager.js +2 -2
- package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.js +2 -2
- package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
- package/dist/src/platforms/whatsappbot/credential-manager.js +2 -2
- package/dist/src/platforms/whatsappbot/credential-manager.js.map +1 -1
- package/dist/src/shared/utils/config-dir.d.ts +14 -0
- package/dist/src/shared/utils/config-dir.d.ts.map +1 -0
- package/dist/src/shared/utils/config-dir.js +22 -0
- package/dist/src/shared/utils/config-dir.js.map +1 -0
- package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
- package/dist/src/shared/utils/derived-key-cache.js +2 -2
- package/dist/src/shared/utils/derived-key-cache.js.map +1 -1
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/telegrambot.mdx +149 -0
- package/docs/content/docs/index.mdx +10 -9
- package/docs/content/docs/quick-start.mdx +2 -0
- package/docs/content/docs/sdk/meta.json +1 -0
- package/docs/content/docs/sdk/telegrambot.mdx +216 -0
- package/e2e/config.ts +24 -0
- package/e2e/helpers.ts +1 -0
- package/e2e/telegrambot.e2e.test.ts +185 -0
- package/examples/telegrambot-listen.ts +54 -0
- package/package.json +10 -2
- package/scripts/postbuild.ts +1 -0
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +12 -5
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +357 -0
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/cli.ts +4 -0
- package/src/platforms/channeltalk/credential-manager.ts +2 -2
- package/src/platforms/channeltalkbot/credential-manager.ts +2 -2
- package/src/platforms/discord/credential-manager.ts +3 -2
- package/src/platforms/discordbot/credential-manager.ts +2 -2
- package/src/platforms/instagram/credential-manager.ts +2 -2
- package/src/platforms/kakaotalk/client.ts +3 -5
- package/src/platforms/kakaotalk/credential-manager.ts +2 -2
- package/src/platforms/line/client.ts +2 -2
- package/src/platforms/line/credential-manager.ts +2 -2
- package/src/platforms/slack/credential-manager.ts +2 -2
- package/src/platforms/slackbot/credential-manager.ts +2 -2
- package/src/platforms/teams/credential-manager.ts +2 -2
- package/src/platforms/telegram/commands/whoami.test.ts +1 -0
- package/src/platforms/telegram/credential-manager.ts +2 -2
- package/src/platforms/telegrambot/cli.ts +34 -0
- package/src/platforms/telegrambot/client.test.ts +454 -0
- package/src/platforms/telegrambot/client.ts +404 -0
- package/src/platforms/telegrambot/commands/auth.test.ts +244 -0
- package/src/platforms/telegrambot/commands/auth.ts +220 -0
- package/src/platforms/telegrambot/commands/chat.ts +96 -0
- package/src/platforms/telegrambot/commands/index.ts +5 -0
- package/src/platforms/telegrambot/commands/message.ts +235 -0
- package/src/platforms/telegrambot/commands/reaction.ts +70 -0
- package/src/platforms/telegrambot/commands/shared.ts +32 -0
- package/src/platforms/telegrambot/commands/whoami.ts +45 -0
- package/src/platforms/telegrambot/credential-manager.test.ts +196 -0
- package/src/platforms/telegrambot/credential-manager.ts +141 -0
- package/src/platforms/telegrambot/index.ts +44 -0
- package/src/platforms/telegrambot/listener.test.ts +398 -0
- package/src/platforms/telegrambot/listener.ts +198 -0
- package/src/platforms/telegrambot/types.test.ts +128 -0
- package/src/platforms/telegrambot/types.ts +282 -0
- package/src/platforms/webex/credential-manager.ts +2 -2
- package/src/platforms/wechatbot/credential-manager.ts +2 -2
- package/src/platforms/whatsapp/credential-manager.ts +2 -2
- package/src/platforms/whatsappbot/credential-manager.ts +2 -2
- package/src/shared/utils/config-dir.test.ts +41 -0
- package/src/shared/utils/config-dir.ts +23 -0
- 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
|
+
})
|