@overpod/mcp-telegram 1.19.0 → 1.21.0
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/CHANGELOG.md +278 -0
- package/dist/__tests__/rate-limiter.test.d.ts +1 -0
- package/dist/__tests__/rate-limiter.test.js +81 -0
- package/dist/index.js +3 -1
- package/dist/rate-limiter.d.ts +26 -0
- package/dist/rate-limiter.js +80 -0
- package/dist/telegram-client.d.ts +5 -1
- package/dist/telegram-client.js +89 -70
- package/dist/tools/messages.js +4 -2
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.21.0] - 2026-04-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `TelegramService.getClient()` — public accessor for the underlying GramJS `TelegramClient` instance, enabling event handlers like `NewMessage` for real-time listeners (#17)
|
|
14
|
+
|
|
15
|
+
## [1.20.0] - 2026-03-31
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Rate limiting & retry** — automatic FLOOD_WAIT handling, network error recovery with exponential backoff (`src/rate-limiter.ts`)
|
|
19
|
+
- `send-message` now returns `messageId` in the response (`Message sent to @user [#12345]`), enabling send → edit workflows (closes #16)
|
|
20
|
+
- Rate limiter unit tests (7 tests in `src/__tests__/rate-limiter.test.ts`)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `sendMessage()` return type changed from `void` to `Api.Message | Api.UpdateShortSentMessage | undefined`
|
|
24
|
+
- Write methods (`sendMessage`, `sendFile`, `editMessage`, `deleteMessages`) are now rate-limited with automatic retry on transient errors
|
|
25
|
+
|
|
26
|
+
## [1.19.0] - 2026-03-30
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Docker support for containerized deployment
|
|
30
|
+
- Non-blocking startup behavior
|
|
31
|
+
- Local QR code fallback for authentication
|
|
32
|
+
- Automated test infrastructure with Node.js test runner
|
|
33
|
+
- CI workflow to publish Docker images to GitHub Container Registry
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Added pnpm-lock.yaml for better dependency management
|
|
37
|
+
|
|
38
|
+
## [1.18.0] - 2026-03-28
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- New `telegram-get-my-role` tool to check user's role in a chat
|
|
42
|
+
- Role information in `telegram-get-chat-members` results
|
|
43
|
+
|
|
44
|
+
## [1.17.0] - 2026-03-28
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- Chat resolution by display name (not just ID or username)
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Updated documentation to replace static tool list with auto-discovery note
|
|
51
|
+
- Improved project structure documentation
|
|
52
|
+
|
|
53
|
+
## [1.16.0] - 2026-03-28
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- Group management tools: invite, kick, ban, edit, leave
|
|
57
|
+
- Admin management capabilities
|
|
58
|
+
|
|
59
|
+
## [1.15.0] - 2026-03-28
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- `telegram-create-group` tool for creating new groups
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
- Documented `AUTH_KEY_DUPLICATED` error handling
|
|
66
|
+
|
|
67
|
+
## [1.14.0] - 2026-03-28
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
- SOCKS5 proxy support for Telegram connections
|
|
71
|
+
- MTProxy support for Telegram connections
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
- Updated Biome to 2.4.9 with new config schema
|
|
75
|
+
- Sorted imports for Biome compliance
|
|
76
|
+
- Added proxy documentation to README
|
|
77
|
+
|
|
78
|
+
## [1.13.0] - 2026-03-26
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- Refactored tools into modular files organized by category
|
|
82
|
+
|
|
83
|
+
## [1.12.0] - 2026-03-26
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
- Migrated to `registerTool()` API with tool annotations
|
|
87
|
+
|
|
88
|
+
## [1.11.1] - 2026-03-25
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
- Sanitized unpaired UTF-16 surrogates in tool responses
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
- Upgraded TypeScript to 6.0
|
|
95
|
+
- Updated README with missing tools
|
|
96
|
+
|
|
97
|
+
## [1.11.0] - 2026-03-23
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
- Full reactions support: read, send multiple reactions, get detailed info
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
- Included message ID in all message-reading tool outputs
|
|
104
|
+
|
|
105
|
+
## [1.10.1] - 2026-03-22
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- Message ID now included in all message-reading tool outputs
|
|
109
|
+
|
|
110
|
+
## [1.10.0] - 2026-03-20
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
- Enhanced `telegram-get-profile` with birthday, business, and premium data
|
|
114
|
+
- New `telegram-get-profile-photo` tool
|
|
115
|
+
- Global message search capability
|
|
116
|
+
- Enriched chat search results
|
|
117
|
+
|
|
118
|
+
## [1.9.0] - 2026-03-18
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
- Forum Topics support
|
|
122
|
+
- Per-topic unread count for forum groups
|
|
123
|
+
- Secure session storage with configurable path
|
|
124
|
+
- Multiple accounts support
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
- Per-topic unread sum calculation for forum groups
|
|
128
|
+
|
|
129
|
+
### Changed
|
|
130
|
+
- Updated session path and security documentation
|
|
131
|
+
- Upgraded GitHub Actions to v6
|
|
132
|
+
- Replaced Node 20 with Node 24 in CI
|
|
133
|
+
- Updated Biome to 2.4.7 and @types/node to 25.5.0
|
|
134
|
+
|
|
135
|
+
## [1.8.1] - 2026-03-19
|
|
136
|
+
|
|
137
|
+
### Fixed
|
|
138
|
+
- Redirected console.log to stderr to prevent MCP JSON-RPC corruption
|
|
139
|
+
|
|
140
|
+
### Changed
|
|
141
|
+
- Updated dependencies (Biome 2.4.8)
|
|
142
|
+
|
|
143
|
+
## [1.8.0] - 2026-03-18
|
|
144
|
+
|
|
145
|
+
### Added
|
|
146
|
+
- Secure session storage with configurable path via SESSION_PATH environment variable
|
|
147
|
+
|
|
148
|
+
### Changed
|
|
149
|
+
- Updated session path and security information in README
|
|
150
|
+
|
|
151
|
+
## [1.7.0] - 2026-03-16
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
- CI workflow to publish to GitHub Packages alongside npm
|
|
155
|
+
- Manual workflow dispatch trigger for publishing
|
|
156
|
+
|
|
157
|
+
## [1.6.0] - 2026-03-16
|
|
158
|
+
|
|
159
|
+
### Added
|
|
160
|
+
- Contact request management
|
|
161
|
+
- Block/unblock users
|
|
162
|
+
- Report spam functionality
|
|
163
|
+
- Add contact tool
|
|
164
|
+
- ChatGPT to list of supported clients
|
|
165
|
+
|
|
166
|
+
### Changed
|
|
167
|
+
- Removed hardcoded tool counts from README and package.json
|
|
168
|
+
- Updated Biome to 2.4.7 and @types/node to 25.5.0
|
|
169
|
+
|
|
170
|
+
## [1.5.0] - 2026-03-16
|
|
171
|
+
|
|
172
|
+
### Added
|
|
173
|
+
- Reactions support
|
|
174
|
+
- Scheduled messages
|
|
175
|
+
- Polls creation and management
|
|
176
|
+
- `telegram-join-chat` tool for joining groups and channels
|
|
177
|
+
|
|
178
|
+
### Changed
|
|
179
|
+
- Updated README with new tool documentation
|
|
180
|
+
- Increased tool count to 24
|
|
181
|
+
|
|
182
|
+
## [1.4.0] - 2026-03-15
|
|
183
|
+
|
|
184
|
+
### Added
|
|
185
|
+
- Glama.ai MCP catalog verification (glama.json)
|
|
186
|
+
- Smithery MCP catalog listing (smithery.yaml)
|
|
187
|
+
- Demo GIF and badges to README
|
|
188
|
+
- Hosted version link
|
|
189
|
+
|
|
190
|
+
### Fixed
|
|
191
|
+
- Removed PNG file save from CLI QR login
|
|
192
|
+
|
|
193
|
+
### Changed
|
|
194
|
+
- Updated README with Glama MCP server badge
|
|
195
|
+
|
|
196
|
+
## [1.3.1] - 2026-03-12
|
|
197
|
+
|
|
198
|
+
### Fixed
|
|
199
|
+
- Use `destroy()` instead of `disconnect()` to stop GramJS update loop
|
|
200
|
+
- Adopt QR login client directly instead of destroy+reconnect flow
|
|
201
|
+
- Destroy GramJS client in `logOut()` and `startQrLogin()` to stop update loop
|
|
202
|
+
|
|
203
|
+
## [1.3.0] - 2026-03-12
|
|
204
|
+
|
|
205
|
+
### Added
|
|
206
|
+
- `logOut()` method for complete Telegram session termination
|
|
207
|
+
|
|
208
|
+
## [1.2.0] - 2026-03-11
|
|
209
|
+
|
|
210
|
+
### Added
|
|
211
|
+
- `downloadMediaAsBuffer` for serverless media download
|
|
212
|
+
- Library exports and declaration types
|
|
213
|
+
- Date filters for messages
|
|
214
|
+
- Comprehensive README for v1.0
|
|
215
|
+
|
|
216
|
+
### Fixed
|
|
217
|
+
- MIME type detection from magic bytes in `downloadMediaAsBuffer`
|
|
218
|
+
- Made `saveSession` resilient to file write errors in Docker
|
|
219
|
+
|
|
220
|
+
### Changed
|
|
221
|
+
- Use `GetFullChannel`/`GetFullChat` for complete chat information
|
|
222
|
+
- Improved `telegram-login` for Claude Desktop users
|
|
223
|
+
- Added npm publishing support and GitHub Actions CI/CD
|
|
224
|
+
|
|
225
|
+
## [1.1.0] - 2026-03-11
|
|
226
|
+
|
|
227
|
+
### Added
|
|
228
|
+
- Contact management tools
|
|
229
|
+
- Chat members listing
|
|
230
|
+
- User profile retrieval
|
|
231
|
+
- Chat type filter
|
|
232
|
+
- Media tools (send, download, get info)
|
|
233
|
+
- Pin/unpin messages
|
|
234
|
+
- Markdown support
|
|
235
|
+
- Media information in messages
|
|
236
|
+
- Unread counts
|
|
237
|
+
- Mark messages as read
|
|
238
|
+
- Forward messages
|
|
239
|
+
- Edit messages
|
|
240
|
+
- Delete messages
|
|
241
|
+
- Detailed chat information
|
|
242
|
+
- Pagination support
|
|
243
|
+
|
|
244
|
+
## [1.0.0] - 2026-03-10
|
|
245
|
+
|
|
246
|
+
### Added
|
|
247
|
+
- Initial release: MCP server for Telegram userbot
|
|
248
|
+
- Basic message reading and sending
|
|
249
|
+
- Chat listing
|
|
250
|
+
- Authentication via phone number and QR code
|
|
251
|
+
- Session persistence
|
|
252
|
+
- GramJS/MTProto integration
|
|
253
|
+
|
|
254
|
+
[Unreleased]: https://github.com/overpod/mcp-telegram/compare/v1.19.0...HEAD
|
|
255
|
+
[1.19.0]: https://github.com/overpod/mcp-telegram/compare/v1.18.0...v1.19.0
|
|
256
|
+
[1.18.0]: https://github.com/overpod/mcp-telegram/compare/v1.17.0...v1.18.0
|
|
257
|
+
[1.17.0]: https://github.com/overpod/mcp-telegram/compare/v1.16.0...v1.17.0
|
|
258
|
+
[1.16.0]: https://github.com/overpod/mcp-telegram/compare/v1.15.0...v1.16.0
|
|
259
|
+
[1.15.0]: https://github.com/overpod/mcp-telegram/compare/v1.14.0...v1.15.0
|
|
260
|
+
[1.14.0]: https://github.com/overpod/mcp-telegram/compare/v1.13.0...v1.14.0
|
|
261
|
+
[1.13.0]: https://github.com/overpod/mcp-telegram/compare/v1.12.0...v1.13.0
|
|
262
|
+
[1.12.0]: https://github.com/overpod/mcp-telegram/compare/v1.11.1...v1.12.0
|
|
263
|
+
[1.11.1]: https://github.com/overpod/mcp-telegram/compare/v1.11.0...v1.11.1
|
|
264
|
+
[1.11.0]: https://github.com/overpod/mcp-telegram/compare/v1.10.1...v1.11.0
|
|
265
|
+
[1.10.1]: https://github.com/overpod/mcp-telegram/compare/v1.10.0...v1.10.1
|
|
266
|
+
[1.10.0]: https://github.com/overpod/mcp-telegram/compare/v1.9.0...v1.10.0
|
|
267
|
+
[1.9.0]: https://github.com/overpod/mcp-telegram/compare/v1.8.1...v1.9.0
|
|
268
|
+
[1.8.1]: https://github.com/overpod/mcp-telegram/compare/v1.8.0...v1.8.1
|
|
269
|
+
[1.8.0]: https://github.com/overpod/mcp-telegram/compare/v1.7.0...v1.8.0
|
|
270
|
+
[1.7.0]: https://github.com/overpod/mcp-telegram/compare/v1.6.0...v1.7.0
|
|
271
|
+
[1.6.0]: https://github.com/overpod/mcp-telegram/compare/v1.5.0...v1.6.0
|
|
272
|
+
[1.5.0]: https://github.com/overpod/mcp-telegram/compare/v1.4.0...v1.5.0
|
|
273
|
+
[1.4.0]: https://github.com/overpod/mcp-telegram/compare/v1.3.1...v1.4.0
|
|
274
|
+
[1.3.1]: https://github.com/overpod/mcp-telegram/compare/v1.3.0...v1.3.1
|
|
275
|
+
[1.3.0]: https://github.com/overpod/mcp-telegram/compare/v1.2.0...v1.3.0
|
|
276
|
+
[1.2.0]: https://github.com/overpod/mcp-telegram/compare/v1.1.0...v1.2.0
|
|
277
|
+
[1.1.0]: https://github.com/overpod/mcp-telegram/compare/v1.0.0...v1.1.0
|
|
278
|
+
[1.0.0]: https://github.com/overpod/mcp-telegram/releases/tag/v1.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { RateLimiter } from "../rate-limiter.js";
|
|
4
|
+
describe("RateLimiter", () => {
|
|
5
|
+
it("should execute a function successfully", async () => {
|
|
6
|
+
const limiter = new RateLimiter({ maxRequestsPerSecond: 100 });
|
|
7
|
+
const result = await limiter.execute(async () => "success");
|
|
8
|
+
assert.strictEqual(result, "success");
|
|
9
|
+
});
|
|
10
|
+
it("should enforce rate limiting between requests", async () => {
|
|
11
|
+
const limiter = new RateLimiter({ maxRequestsPerSecond: 10 }); // 10 req/s = 100ms between requests
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
await limiter.execute(async () => "first");
|
|
14
|
+
await limiter.execute(async () => "second");
|
|
15
|
+
const elapsed = Date.now() - start;
|
|
16
|
+
assert.ok(elapsed >= 90, `Expected at least 90ms, got ${elapsed}ms`);
|
|
17
|
+
});
|
|
18
|
+
it("should retry on FLOOD_WAIT error", async () => {
|
|
19
|
+
const limiter = new RateLimiter({ maxRetries: 2, maxRequestsPerSecond: 100 });
|
|
20
|
+
let attempts = 0;
|
|
21
|
+
const result = await limiter.execute(async () => {
|
|
22
|
+
attempts++;
|
|
23
|
+
if (attempts < 2) {
|
|
24
|
+
throw new Error("FLOOD_WAIT_1");
|
|
25
|
+
}
|
|
26
|
+
return "success after retry";
|
|
27
|
+
});
|
|
28
|
+
assert.strictEqual(result, "success after retry");
|
|
29
|
+
assert.strictEqual(attempts, 2);
|
|
30
|
+
});
|
|
31
|
+
it("should throw after max retries on FLOOD_WAIT", async () => {
|
|
32
|
+
const limiter = new RateLimiter({ maxRetries: 1, maxRequestsPerSecond: 100 });
|
|
33
|
+
await assert.rejects(async () => {
|
|
34
|
+
await limiter.execute(async () => {
|
|
35
|
+
throw new Error("FLOOD_WAIT_2");
|
|
36
|
+
});
|
|
37
|
+
}, {
|
|
38
|
+
message: /Rate limit exceeded after 1 retries/,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it("should retry on network errors with exponential backoff", async () => {
|
|
42
|
+
const limiter = new RateLimiter({
|
|
43
|
+
maxRetries: 2,
|
|
44
|
+
initialRetryDelay: 100,
|
|
45
|
+
maxRequestsPerSecond: 100,
|
|
46
|
+
});
|
|
47
|
+
let attempts = 0;
|
|
48
|
+
const result = await limiter.execute(async () => {
|
|
49
|
+
attempts++;
|
|
50
|
+
if (attempts < 2) {
|
|
51
|
+
throw new Error("TIMEOUT");
|
|
52
|
+
}
|
|
53
|
+
return "recovered";
|
|
54
|
+
});
|
|
55
|
+
assert.strictEqual(result, "recovered");
|
|
56
|
+
assert.strictEqual(attempts, 2);
|
|
57
|
+
});
|
|
58
|
+
it("should not retry on non-retryable errors", async () => {
|
|
59
|
+
const limiter = new RateLimiter({ maxRetries: 3, maxRequestsPerSecond: 100 });
|
|
60
|
+
let attempts = 0;
|
|
61
|
+
await assert.rejects(async () => {
|
|
62
|
+
await limiter.execute(async () => {
|
|
63
|
+
attempts++;
|
|
64
|
+
throw new Error("AUTH_KEY_UNREGISTERED");
|
|
65
|
+
});
|
|
66
|
+
}, {
|
|
67
|
+
message: "AUTH_KEY_UNREGISTERED",
|
|
68
|
+
});
|
|
69
|
+
assert.strictEqual(attempts, 1, "Should not retry non-retryable errors");
|
|
70
|
+
});
|
|
71
|
+
it("should handle FLOOD_WAIT with seconds parsing", async () => {
|
|
72
|
+
const limiter = new RateLimiter({ maxRetries: 1, maxRequestsPerSecond: 100 });
|
|
73
|
+
await assert.rejects(async () => {
|
|
74
|
+
await limiter.execute(async () => {
|
|
75
|
+
throw new Error("FLOOD_WAIT_1");
|
|
76
|
+
});
|
|
77
|
+
}, {
|
|
78
|
+
message: /Telegram requires 1s wait/,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,9 @@ import { registerTools } from "./tools/index.js";
|
|
|
14
14
|
const API_ID = Number(process.env.TELEGRAM_API_ID);
|
|
15
15
|
const API_HASH = process.env.TELEGRAM_API_HASH;
|
|
16
16
|
if (!API_ID || !API_HASH) {
|
|
17
|
-
console.error("[mcp-telegram] TELEGRAM_API_ID and TELEGRAM_API_HASH
|
|
17
|
+
console.error("[mcp-telegram] Missing TELEGRAM_API_ID and TELEGRAM_API_HASH");
|
|
18
|
+
console.error("Get your credentials at https://my.telegram.org/apps (API development tools)");
|
|
19
|
+
console.error("Set them in .env or export as environment variables");
|
|
18
20
|
process.exit(1);
|
|
19
21
|
}
|
|
20
22
|
const telegram = new TelegramService(API_ID, API_HASH);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter and retry logic for Telegram API calls.
|
|
3
|
+
* Handles FLOOD_WAIT errors and implements exponential backoff.
|
|
4
|
+
*/
|
|
5
|
+
export interface RateLimiterOptions {
|
|
6
|
+
/** Maximum number of requests per second (default: 20) */
|
|
7
|
+
maxRequestsPerSecond?: number;
|
|
8
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
/** Initial retry delay in milliseconds (default: 1000) */
|
|
11
|
+
initialRetryDelay?: number;
|
|
12
|
+
/** Maximum retry delay in milliseconds (default: 60000) */
|
|
13
|
+
maxRetryDelay?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class RateLimiter {
|
|
16
|
+
private lastRequestTime;
|
|
17
|
+
private minInterval;
|
|
18
|
+
private maxRetries;
|
|
19
|
+
private initialRetryDelay;
|
|
20
|
+
private maxRetryDelay;
|
|
21
|
+
constructor(options?: RateLimiterOptions);
|
|
22
|
+
/** Execute a function with rate limiting and automatic retry */
|
|
23
|
+
execute<T>(fn: () => Promise<T>, context?: string): Promise<T>;
|
|
24
|
+
private executeWithRetry;
|
|
25
|
+
private waitForSlot;
|
|
26
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter and retry logic for Telegram API calls.
|
|
3
|
+
* Handles FLOOD_WAIT errors and implements exponential backoff.
|
|
4
|
+
*/
|
|
5
|
+
export class RateLimiter {
|
|
6
|
+
lastRequestTime = 0;
|
|
7
|
+
minInterval;
|
|
8
|
+
maxRetries;
|
|
9
|
+
initialRetryDelay;
|
|
10
|
+
maxRetryDelay;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
const maxRequestsPerSecond = options.maxRequestsPerSecond ?? 20;
|
|
13
|
+
this.minInterval = 1000 / maxRequestsPerSecond;
|
|
14
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
15
|
+
this.initialRetryDelay = options.initialRetryDelay ?? 1000;
|
|
16
|
+
this.maxRetryDelay = options.maxRetryDelay ?? 60000;
|
|
17
|
+
}
|
|
18
|
+
/** Execute a function with rate limiting and automatic retry */
|
|
19
|
+
async execute(fn, context = "API call") {
|
|
20
|
+
return this.executeWithRetry(fn, context, 0);
|
|
21
|
+
}
|
|
22
|
+
async executeWithRetry(fn, context, attempt) {
|
|
23
|
+
await this.waitForSlot();
|
|
24
|
+
try {
|
|
25
|
+
return await fn();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const errorMessage = error.errorMessage || error.message || String(error);
|
|
29
|
+
// FLOOD_WAIT — wait the exact time Telegram requires
|
|
30
|
+
const floodMatch = errorMessage.match(/FLOOD_WAIT[_]?(\d+)/i);
|
|
31
|
+
if (floodMatch) {
|
|
32
|
+
const waitSeconds = Number.parseInt(floodMatch[1], 10);
|
|
33
|
+
if (attempt >= this.maxRetries) {
|
|
34
|
+
throw new Error(`Rate limit exceeded after ${this.maxRetries} retries. Telegram requires ${waitSeconds}s wait. Try again later.`);
|
|
35
|
+
}
|
|
36
|
+
console.error(`[rate-limiter] FLOOD_WAIT for ${context}. Waiting ${waitSeconds}s (attempt ${attempt + 1}/${this.maxRetries})`);
|
|
37
|
+
await sleep(waitSeconds * 1000);
|
|
38
|
+
return this.executeWithRetry(fn, context, attempt + 1);
|
|
39
|
+
}
|
|
40
|
+
// Network/timeout errors — exponential backoff
|
|
41
|
+
if (isNetworkError(errorMessage)) {
|
|
42
|
+
if (attempt >= this.maxRetries) {
|
|
43
|
+
throw new Error(`Network error after ${this.maxRetries} retries: ${errorMessage}. Check your connection.`);
|
|
44
|
+
}
|
|
45
|
+
const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
|
|
46
|
+
console.error(`[rate-limiter] Network error for ${context}. Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`);
|
|
47
|
+
await sleep(delay);
|
|
48
|
+
return this.executeWithRetry(fn, context, attempt + 1);
|
|
49
|
+
}
|
|
50
|
+
// Temporary server errors (5xx) — exponential backoff
|
|
51
|
+
if (isTemporaryError(errorMessage)) {
|
|
52
|
+
if (attempt >= this.maxRetries) {
|
|
53
|
+
throw new Error(`Temporary error after ${this.maxRetries} retries: ${errorMessage}`);
|
|
54
|
+
}
|
|
55
|
+
const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
|
|
56
|
+
await sleep(delay);
|
|
57
|
+
return this.executeWithRetry(fn, context, attempt + 1);
|
|
58
|
+
}
|
|
59
|
+
// Non-retryable — throw immediately
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async waitForSlot() {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const elapsed = now - this.lastRequestTime;
|
|
66
|
+
if (elapsed < this.minInterval) {
|
|
67
|
+
await sleep(this.minInterval - elapsed);
|
|
68
|
+
}
|
|
69
|
+
this.lastRequestTime = Date.now();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function isNetworkError(msg) {
|
|
73
|
+
return /TIMEOUT|ETIMEDOUT|ECONNREFUSED|ENETUNREACH|ENOTFOUND|EHOSTUNREACH|network|timed out/i.test(msg);
|
|
74
|
+
}
|
|
75
|
+
function isTemporaryError(msg) {
|
|
76
|
+
return /INTERNAL|^50[023]$|Internal Server Error|Service Unavailable|Bad Gateway/i.test(msg);
|
|
77
|
+
}
|
|
78
|
+
function sleep(ms) {
|
|
79
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
80
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TelegramClient } from "telegram";
|
|
2
|
+
import { Api } from "telegram/tl/index.js";
|
|
1
3
|
export declare class TelegramService {
|
|
2
4
|
private client;
|
|
3
5
|
private apiId;
|
|
@@ -5,8 +7,10 @@ export declare class TelegramService {
|
|
|
5
7
|
private sessionString;
|
|
6
8
|
private connected;
|
|
7
9
|
private sessionPath;
|
|
10
|
+
private rateLimiter;
|
|
8
11
|
lastError: string;
|
|
9
12
|
get sessionDir(): string;
|
|
13
|
+
getClient(): TelegramClient | null;
|
|
10
14
|
constructor(apiId: number, apiHash: string, options?: {
|
|
11
15
|
sessionPath?: string;
|
|
12
16
|
});
|
|
@@ -37,7 +41,7 @@ export declare class TelegramService {
|
|
|
37
41
|
username?: string;
|
|
38
42
|
firstName?: string;
|
|
39
43
|
}>;
|
|
40
|
-
sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<
|
|
44
|
+
sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<Api.Message | Api.UpdateShortSentMessage | undefined>;
|
|
41
45
|
sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
|
|
42
46
|
downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
|
|
43
47
|
downloadMediaAsBuffer(chatId: string, messageId: number): Promise<{
|
package/dist/telegram-client.js
CHANGED
|
@@ -9,12 +9,14 @@ import { TelegramClient } from "telegram";
|
|
|
9
9
|
import { CustomFile } from "telegram/client/uploads.js";
|
|
10
10
|
import { StringSession } from "telegram/sessions/index.js";
|
|
11
11
|
import { Api } from "telegram/tl/index.js";
|
|
12
|
+
import { RateLimiter } from "./rate-limiter.js";
|
|
12
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const LEGACY_SESSION_FILE = join(__dirname, "..", ".telegram-session");
|
|
14
15
|
const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
|
|
15
16
|
const DEFAULT_SESSION_FILE = join(DEFAULT_SESSION_DIR, "session");
|
|
16
17
|
const SESSION_STRING_RE = /^[A-Za-z0-9+/=]+$/;
|
|
17
18
|
const MIN_SESSION_LENGTH = 100;
|
|
19
|
+
const NOT_CONNECTED_ERROR = "Not connected. Run telegram-status to check or telegram-login to authenticate.";
|
|
18
20
|
function resolveSessionPath(sessionPath) {
|
|
19
21
|
return sessionPath ?? process.env.TELEGRAM_SESSION_PATH ?? DEFAULT_SESSION_FILE;
|
|
20
22
|
}
|
|
@@ -49,10 +51,14 @@ export class TelegramService {
|
|
|
49
51
|
sessionString = "";
|
|
50
52
|
connected = false;
|
|
51
53
|
sessionPath;
|
|
54
|
+
rateLimiter = new RateLimiter();
|
|
52
55
|
lastError = "";
|
|
53
56
|
get sessionDir() {
|
|
54
57
|
return dirname(this.sessionPath);
|
|
55
58
|
}
|
|
59
|
+
getClient() {
|
|
60
|
+
return this.client;
|
|
61
|
+
}
|
|
56
62
|
constructor(apiId, apiHash, options) {
|
|
57
63
|
this.apiId = apiId;
|
|
58
64
|
this.apiHash = apiHash;
|
|
@@ -136,7 +142,7 @@ export class TelegramService {
|
|
|
136
142
|
// Auth revoked — delete invalid session
|
|
137
143
|
if (msg === "AUTH_KEY_UNREGISTERED" || msg === "SESSION_REVOKED" || msg === "USER_DEACTIVATED") {
|
|
138
144
|
await this.clearSession();
|
|
139
|
-
this.lastError = "Session revoked.
|
|
145
|
+
this.lastError = "Session revoked. Run telegram-login to re-authenticate.";
|
|
140
146
|
}
|
|
141
147
|
// Network error — keep session, just report
|
|
142
148
|
else if (msg.includes("TIMEOUT") ||
|
|
@@ -144,7 +150,7 @@ export class TelegramService {
|
|
|
144
150
|
msg.includes("ENETUNREACH") ||
|
|
145
151
|
msg.includes("ENOTFOUND") ||
|
|
146
152
|
msg.includes("network")) {
|
|
147
|
-
this.lastError = `Network error: ${msg}.
|
|
153
|
+
this.lastError = `Network error: ${msg}. Run telegram-status to retry connection.`;
|
|
148
154
|
}
|
|
149
155
|
// Unknown error
|
|
150
156
|
else {
|
|
@@ -290,7 +296,7 @@ export class TelegramService {
|
|
|
290
296
|
}
|
|
291
297
|
async getMe() {
|
|
292
298
|
if (!this.client || !this.connected)
|
|
293
|
-
throw new Error(
|
|
299
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
294
300
|
const me = await this.client.getMe();
|
|
295
301
|
const user = me;
|
|
296
302
|
return {
|
|
@@ -301,38 +307,47 @@ export class TelegramService {
|
|
|
301
307
|
}
|
|
302
308
|
async sendMessage(chatId, text, replyTo, parseMode, topicId) {
|
|
303
309
|
if (!this.client || !this.connected)
|
|
304
|
-
throw new Error(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
310
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
311
|
+
return this.rateLimiter.execute(async () => {
|
|
312
|
+
const resolved = await this.resolvePeer(chatId);
|
|
313
|
+
if (topicId) {
|
|
314
|
+
const peer = await this.client?.getInputEntity(resolved);
|
|
315
|
+
const result = await this.client?.invoke(new Api.messages.SendMessage({
|
|
316
|
+
peer,
|
|
317
|
+
message: text,
|
|
318
|
+
randomId: bigInt(Math.floor(Math.random() * 1e15)),
|
|
319
|
+
replyTo: new Api.InputReplyToMessage({
|
|
320
|
+
replyToMsgId: replyTo ?? topicId,
|
|
321
|
+
topMsgId: topicId,
|
|
322
|
+
}),
|
|
323
|
+
}));
|
|
324
|
+
if (result instanceof Api.UpdateShortSentMessage)
|
|
325
|
+
return result;
|
|
326
|
+
if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
|
|
327
|
+
const msgUpdate = result.updates.find((u) => u instanceof Api.UpdateNewMessage);
|
|
328
|
+
if (msgUpdate?.message instanceof Api.Message)
|
|
329
|
+
return msgUpdate.message;
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
return await this.client?.sendMessage(resolved, {
|
|
321
334
|
message: text,
|
|
322
335
|
...(replyTo ? { replyTo } : {}),
|
|
323
336
|
...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
|
|
324
337
|
});
|
|
325
|
-
}
|
|
338
|
+
}, `sendMessage to ${chatId}`);
|
|
326
339
|
}
|
|
327
340
|
async sendFile(chatId, filePath, caption) {
|
|
328
341
|
if (!this.client || !this.connected)
|
|
329
|
-
throw new Error(
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
343
|
+
await this.rateLimiter.execute(async () => {
|
|
344
|
+
const resolved = await this.resolvePeer(chatId);
|
|
345
|
+
await this.client?.sendFile(resolved, { file: filePath, caption });
|
|
346
|
+
}, `sendFile to ${chatId}`);
|
|
332
347
|
}
|
|
333
348
|
async downloadMedia(chatId, messageId, downloadPath) {
|
|
334
349
|
if (!this.client || !this.connected)
|
|
335
|
-
throw new Error(
|
|
350
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
336
351
|
const resolved = await this.resolvePeer(chatId);
|
|
337
352
|
const messages = await this.client.getMessages(resolved, { ids: [messageId] });
|
|
338
353
|
const message = messages[0];
|
|
@@ -348,7 +363,7 @@ export class TelegramService {
|
|
|
348
363
|
}
|
|
349
364
|
async downloadMediaAsBuffer(chatId, messageId) {
|
|
350
365
|
if (!this.client || !this.connected)
|
|
351
|
-
throw new Error(
|
|
366
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
352
367
|
const resolved = await this.resolvePeer(chatId);
|
|
353
368
|
const messages = await this.client.getMessages(resolved, { ids: [messageId] });
|
|
354
369
|
const message = messages[0];
|
|
@@ -384,19 +399,19 @@ export class TelegramService {
|
|
|
384
399
|
}
|
|
385
400
|
async pinMessage(chatId, messageId, silent = false) {
|
|
386
401
|
if (!this.client || !this.connected)
|
|
387
|
-
throw new Error(
|
|
402
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
388
403
|
const resolved = await this.resolvePeer(chatId);
|
|
389
404
|
await this.client.pinMessage(resolved, messageId, { notify: !silent });
|
|
390
405
|
}
|
|
391
406
|
async unpinMessage(chatId, messageId) {
|
|
392
407
|
if (!this.client || !this.connected)
|
|
393
|
-
throw new Error(
|
|
408
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
394
409
|
const resolved = await this.resolvePeer(chatId);
|
|
395
410
|
await this.client.unpinMessage(resolved, messageId);
|
|
396
411
|
}
|
|
397
412
|
async getDialogs(limit = 20, offsetDate, filterType) {
|
|
398
413
|
if (!this.client || !this.connected)
|
|
399
|
-
throw new Error(
|
|
414
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
400
415
|
const fetchLimit = filterType ? limit * 3 : limit;
|
|
401
416
|
const dialogs = await this.client.getDialogs({ limit: fetchLimit, ...(offsetDate ? { offsetDate } : {}) });
|
|
402
417
|
const mapped = dialogs.map((d) => {
|
|
@@ -419,7 +434,7 @@ export class TelegramService {
|
|
|
419
434
|
}
|
|
420
435
|
async getUnreadDialogs(limit = 20) {
|
|
421
436
|
if (!this.client || !this.connected)
|
|
422
|
-
throw new Error(
|
|
437
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
423
438
|
const dialogs = await this.client.getDialogs({ limit: limit * 3 });
|
|
424
439
|
const unread = dialogs.filter((d) => d.unreadCount > 0).slice(0, limit);
|
|
425
440
|
const results = await Promise.all(unread.map(async (d) => {
|
|
@@ -460,7 +475,7 @@ export class TelegramService {
|
|
|
460
475
|
}
|
|
461
476
|
async getContactRequests(limit = 20) {
|
|
462
477
|
if (!this.client || !this.connected)
|
|
463
|
-
throw new Error(
|
|
478
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
464
479
|
const dialogs = await this.client.getDialogs({ limit: limit * 5 });
|
|
465
480
|
return dialogs
|
|
466
481
|
.filter((d) => {
|
|
@@ -485,7 +500,7 @@ export class TelegramService {
|
|
|
485
500
|
}
|
|
486
501
|
async addContact(userId, firstName, lastName, phone) {
|
|
487
502
|
if (!this.client || !this.connected)
|
|
488
|
-
throw new Error(
|
|
503
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
489
504
|
const entity = await this.client.getInputEntity(userId);
|
|
490
505
|
await this.client.invoke(new Api.contacts.AddContact({
|
|
491
506
|
id: entity,
|
|
@@ -496,39 +511,43 @@ export class TelegramService {
|
|
|
496
511
|
}
|
|
497
512
|
async blockUser(userId) {
|
|
498
513
|
if (!this.client || !this.connected)
|
|
499
|
-
throw new Error(
|
|
514
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
500
515
|
const entity = await this.client.getInputEntity(userId);
|
|
501
516
|
await this.client.invoke(new Api.contacts.Block({ id: entity }));
|
|
502
517
|
}
|
|
503
518
|
async reportSpam(chatId) {
|
|
504
519
|
if (!this.client || !this.connected)
|
|
505
|
-
throw new Error(
|
|
520
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
506
521
|
const peer = await this.client.getInputEntity(chatId);
|
|
507
522
|
await this.client.invoke(new Api.messages.ReportSpam({ peer }));
|
|
508
523
|
}
|
|
509
524
|
async markAsRead(chatId) {
|
|
510
525
|
if (!this.client || !this.connected)
|
|
511
|
-
throw new Error(
|
|
526
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
512
527
|
await this.client.markAsRead(chatId);
|
|
513
528
|
}
|
|
514
529
|
async forwardMessage(fromChatId, toChatId, messageIds) {
|
|
515
530
|
if (!this.client || !this.connected)
|
|
516
|
-
throw new Error(
|
|
531
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
517
532
|
const resolvedFrom = await this.resolvePeer(fromChatId);
|
|
518
533
|
const resolvedTo = await this.resolvePeer(toChatId);
|
|
519
534
|
await this.client.forwardMessages(resolvedTo, { messages: messageIds, fromPeer: resolvedFrom });
|
|
520
535
|
}
|
|
521
536
|
async editMessage(chatId, messageId, newText) {
|
|
522
537
|
if (!this.client || !this.connected)
|
|
523
|
-
throw new Error(
|
|
524
|
-
|
|
525
|
-
|
|
538
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
539
|
+
await this.rateLimiter.execute(async () => {
|
|
540
|
+
const resolved = await this.resolvePeer(chatId);
|
|
541
|
+
await this.client?.editMessage(resolved, { message: messageId, text: newText });
|
|
542
|
+
}, `editMessage ${messageId} in ${chatId}`);
|
|
526
543
|
}
|
|
527
544
|
async deleteMessages(chatId, messageIds) {
|
|
528
545
|
if (!this.client || !this.connected)
|
|
529
|
-
throw new Error(
|
|
530
|
-
|
|
531
|
-
|
|
546
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
547
|
+
await this.rateLimiter.execute(async () => {
|
|
548
|
+
const resolved = await this.resolvePeer(chatId);
|
|
549
|
+
await this.client?.deleteMessages(resolved, messageIds, { revoke: true });
|
|
550
|
+
}, `deleteMessages in ${chatId}`);
|
|
532
551
|
}
|
|
533
552
|
/**
|
|
534
553
|
* Resolve a chat by ID, username, or display name.
|
|
@@ -537,7 +556,7 @@ export class TelegramService {
|
|
|
537
556
|
// biome-ignore lint: GramJS has no proper entity union type
|
|
538
557
|
async resolveChat(chatId) {
|
|
539
558
|
if (!this.client)
|
|
540
|
-
throw new Error(
|
|
559
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
541
560
|
// First try direct resolve (numeric ID, username, phone)
|
|
542
561
|
try {
|
|
543
562
|
return await this.client.getEntity(chatId);
|
|
@@ -576,7 +595,7 @@ export class TelegramService {
|
|
|
576
595
|
}
|
|
577
596
|
async getChatInfo(chatId) {
|
|
578
597
|
if (!this.client || !this.connected)
|
|
579
|
-
throw new Error(
|
|
598
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
580
599
|
const entity = await this.resolveChat(chatId);
|
|
581
600
|
if (entity instanceof Api.User) {
|
|
582
601
|
const parts = [entity.firstName, entity.lastName].filter(Boolean);
|
|
@@ -684,7 +703,7 @@ export class TelegramService {
|
|
|
684
703
|
}
|
|
685
704
|
async getMessages(chatId, limit = 10, offsetId, minDate, maxDate) {
|
|
686
705
|
if (!this.client || !this.connected)
|
|
687
|
-
throw new Error(
|
|
706
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
688
707
|
const resolved = await this.resolvePeer(chatId);
|
|
689
708
|
const opts = {
|
|
690
709
|
limit,
|
|
@@ -708,7 +727,7 @@ export class TelegramService {
|
|
|
708
727
|
}
|
|
709
728
|
async searchChats(query, limit = 10) {
|
|
710
729
|
if (!this.client || !this.connected)
|
|
711
|
-
throw new Error(
|
|
730
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
712
731
|
const result = await this.client.invoke(new Api.contacts.Search({ q: query, limit }));
|
|
713
732
|
const chats = [];
|
|
714
733
|
for (const user of result.users) {
|
|
@@ -769,7 +788,7 @@ export class TelegramService {
|
|
|
769
788
|
}
|
|
770
789
|
async searchGlobal(query, limit = 20, minDate, maxDate) {
|
|
771
790
|
if (!this.client || !this.connected)
|
|
772
|
-
throw new Error(
|
|
791
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
773
792
|
const result = await this.client.invoke(new Api.messages.SearchGlobal({
|
|
774
793
|
q: query,
|
|
775
794
|
filter: new Api.InputMessagesFilterEmpty(),
|
|
@@ -838,7 +857,7 @@ export class TelegramService {
|
|
|
838
857
|
}
|
|
839
858
|
async searchMessages(chatId, query, limit = 20, minDate, maxDate) {
|
|
840
859
|
if (!this.client || !this.connected)
|
|
841
|
-
throw new Error(
|
|
860
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
842
861
|
const resolved = await this.resolvePeer(chatId);
|
|
843
862
|
const messages = await this.client.getMessages(resolved, {
|
|
844
863
|
search: query,
|
|
@@ -861,7 +880,7 @@ export class TelegramService {
|
|
|
861
880
|
}
|
|
862
881
|
async getContacts(limit = 50) {
|
|
863
882
|
if (!this.client || !this.connected)
|
|
864
|
-
throw new Error(
|
|
883
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
865
884
|
const result = await this.client.invoke(new Api.contacts.GetContacts({ hash: bigInt(0) }));
|
|
866
885
|
if (!(result instanceof Api.contacts.Contacts))
|
|
867
886
|
return [];
|
|
@@ -881,7 +900,7 @@ export class TelegramService {
|
|
|
881
900
|
}
|
|
882
901
|
async getChatMembers(chatId, limit = 50) {
|
|
883
902
|
if (!this.client || !this.connected)
|
|
884
|
-
throw new Error(
|
|
903
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
885
904
|
const entity = await this.resolveChat(chatId);
|
|
886
905
|
if (entity instanceof Api.Channel) {
|
|
887
906
|
const result = await this.client.invoke(new Api.channels.GetParticipants({
|
|
@@ -926,7 +945,7 @@ export class TelegramService {
|
|
|
926
945
|
}
|
|
927
946
|
async getMyRole(chatId) {
|
|
928
947
|
if (!this.client || !this.connected)
|
|
929
|
-
throw new Error(
|
|
948
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
930
949
|
const entity = await this.resolveChat(chatId);
|
|
931
950
|
const me = await this.getMe();
|
|
932
951
|
if (entity instanceof Api.Channel) {
|
|
@@ -978,7 +997,7 @@ export class TelegramService {
|
|
|
978
997
|
}
|
|
979
998
|
async getProfile(userId) {
|
|
980
999
|
if (!this.client || !this.connected)
|
|
981
|
-
throw new Error(
|
|
1000
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
982
1001
|
const entity = await this.client.getEntity(userId);
|
|
983
1002
|
if (!(entity instanceof Api.User))
|
|
984
1003
|
throw new Error("Entity is not a user");
|
|
@@ -1038,7 +1057,7 @@ export class TelegramService {
|
|
|
1038
1057
|
}
|
|
1039
1058
|
async downloadProfilePhoto(entityId, options) {
|
|
1040
1059
|
if (!this.client || !this.connected)
|
|
1041
|
-
throw new Error(
|
|
1060
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1042
1061
|
const entity = await this.client.getEntity(entityId);
|
|
1043
1062
|
const buffer = (await this.client.downloadProfilePhoto(entity, {
|
|
1044
1063
|
isBig: options?.isBig !== false,
|
|
@@ -1089,7 +1108,7 @@ export class TelegramService {
|
|
|
1089
1108
|
}
|
|
1090
1109
|
async sendReaction(chatId, messageId, emoji, addToExisting = false) {
|
|
1091
1110
|
if (!this.client || !this.connected)
|
|
1092
|
-
throw new Error(
|
|
1111
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1093
1112
|
const resolved = await this.resolvePeer(chatId);
|
|
1094
1113
|
const peer = await this.client.getInputEntity(resolved);
|
|
1095
1114
|
const reactionList = [];
|
|
@@ -1129,7 +1148,7 @@ export class TelegramService {
|
|
|
1129
1148
|
}
|
|
1130
1149
|
async getMessageReactions(chatId, messageId) {
|
|
1131
1150
|
if (!this.client || !this.connected)
|
|
1132
|
-
throw new Error(
|
|
1151
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1133
1152
|
const resolved = await this.resolvePeer(chatId);
|
|
1134
1153
|
const peer = await this.client.getInputEntity(resolved);
|
|
1135
1154
|
// First get the message to know which reactions exist
|
|
@@ -1184,7 +1203,7 @@ export class TelegramService {
|
|
|
1184
1203
|
}
|
|
1185
1204
|
async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
|
|
1186
1205
|
if (!this.client || !this.connected)
|
|
1187
|
-
throw new Error(
|
|
1206
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1188
1207
|
const resolved = await this.resolvePeer(chatId);
|
|
1189
1208
|
await this.client.sendMessage(resolved, {
|
|
1190
1209
|
message: text,
|
|
@@ -1195,7 +1214,7 @@ export class TelegramService {
|
|
|
1195
1214
|
}
|
|
1196
1215
|
async createPoll(chatId, question, answers, options) {
|
|
1197
1216
|
if (!this.client || !this.connected)
|
|
1198
|
-
throw new Error(
|
|
1217
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1199
1218
|
const peer = await this.client.getInputEntity(chatId);
|
|
1200
1219
|
const pollAnswers = answers.map((text, i) => new Api.PollAnswer({
|
|
1201
1220
|
text: new Api.TextWithEntities({ text, entities: [] }),
|
|
@@ -1231,7 +1250,7 @@ export class TelegramService {
|
|
|
1231
1250
|
}
|
|
1232
1251
|
async getForumTopics(chatId, limit = 100) {
|
|
1233
1252
|
if (!this.client || !this.connected)
|
|
1234
|
-
throw new Error(
|
|
1253
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1235
1254
|
const entity = await this.resolveChat(chatId);
|
|
1236
1255
|
if (!(entity instanceof Api.Channel))
|
|
1237
1256
|
throw new Error("Forum topics are only available in supergroups");
|
|
@@ -1260,7 +1279,7 @@ export class TelegramService {
|
|
|
1260
1279
|
}
|
|
1261
1280
|
async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
|
|
1262
1281
|
if (!this.client || !this.connected)
|
|
1263
|
-
throw new Error(
|
|
1282
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1264
1283
|
const peer = await this.client.getInputEntity(chatId);
|
|
1265
1284
|
const result = await this.client.invoke(new Api.messages.GetReplies({
|
|
1266
1285
|
peer,
|
|
@@ -1289,7 +1308,7 @@ export class TelegramService {
|
|
|
1289
1308
|
/** Check if a chat entity is a forum (has topics enabled) */
|
|
1290
1309
|
async isForum(chatId) {
|
|
1291
1310
|
if (!this.client || !this.connected)
|
|
1292
|
-
throw new Error(
|
|
1311
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1293
1312
|
try {
|
|
1294
1313
|
const entity = await this.resolveChat(chatId);
|
|
1295
1314
|
if (entity instanceof Api.Channel) {
|
|
@@ -1301,7 +1320,7 @@ export class TelegramService {
|
|
|
1301
1320
|
}
|
|
1302
1321
|
async joinChat(target) {
|
|
1303
1322
|
if (!this.client)
|
|
1304
|
-
throw new Error(
|
|
1323
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1305
1324
|
// Extract invite hash from various link formats
|
|
1306
1325
|
const inviteMatch = target.match(/(?:t\.me\/\+|t\.me\/joinchat\/|tg:\/\/join\?invite=)([a-zA-Z0-9_-]+)/);
|
|
1307
1326
|
if (inviteMatch) {
|
|
@@ -1332,7 +1351,7 @@ export class TelegramService {
|
|
|
1332
1351
|
}
|
|
1333
1352
|
async createGroup(options) {
|
|
1334
1353
|
if (!this.client)
|
|
1335
|
-
throw new Error(
|
|
1354
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1336
1355
|
const { title, users, supergroup = false, forum = false, description } = options;
|
|
1337
1356
|
if (supergroup || forum) {
|
|
1338
1357
|
// Create supergroup/channel via channels.CreateChannel
|
|
@@ -1406,7 +1425,7 @@ export class TelegramService {
|
|
|
1406
1425
|
}
|
|
1407
1426
|
async inviteToGroup(chatId, users) {
|
|
1408
1427
|
if (!this.client)
|
|
1409
|
-
throw new Error(
|
|
1428
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1410
1429
|
const entity = await this.resolveChat(chatId);
|
|
1411
1430
|
const invited = [];
|
|
1412
1431
|
const failed = [];
|
|
@@ -1434,7 +1453,7 @@ export class TelegramService {
|
|
|
1434
1453
|
}
|
|
1435
1454
|
async kickUser(chatId, userId) {
|
|
1436
1455
|
if (!this.client)
|
|
1437
|
-
throw new Error(
|
|
1456
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1438
1457
|
const entity = await this.resolveChat(chatId);
|
|
1439
1458
|
const user = await this.client.getEntity(userId);
|
|
1440
1459
|
if (!(user instanceof Api.User))
|
|
@@ -1459,7 +1478,7 @@ export class TelegramService {
|
|
|
1459
1478
|
}
|
|
1460
1479
|
async banUser(chatId, userId) {
|
|
1461
1480
|
if (!this.client)
|
|
1462
|
-
throw new Error(
|
|
1481
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1463
1482
|
const entity = await this.resolveChat(chatId);
|
|
1464
1483
|
const user = await this.client.getEntity(userId);
|
|
1465
1484
|
if (!(user instanceof Api.User))
|
|
@@ -1475,7 +1494,7 @@ export class TelegramService {
|
|
|
1475
1494
|
}
|
|
1476
1495
|
async unbanUser(chatId, userId) {
|
|
1477
1496
|
if (!this.client)
|
|
1478
|
-
throw new Error(
|
|
1497
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1479
1498
|
const entity = await this.resolveChat(chatId);
|
|
1480
1499
|
const user = await this.client.getEntity(userId);
|
|
1481
1500
|
if (!(user instanceof Api.User))
|
|
@@ -1491,7 +1510,7 @@ export class TelegramService {
|
|
|
1491
1510
|
}
|
|
1492
1511
|
async editGroup(chatId, options) {
|
|
1493
1512
|
if (!this.client)
|
|
1494
|
-
throw new Error(
|
|
1513
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1495
1514
|
const entity = await this.resolveChat(chatId);
|
|
1496
1515
|
if (options.title) {
|
|
1497
1516
|
if (entity instanceof Api.Channel) {
|
|
@@ -1521,7 +1540,7 @@ export class TelegramService {
|
|
|
1521
1540
|
}
|
|
1522
1541
|
async leaveGroup(chatId) {
|
|
1523
1542
|
if (!this.client)
|
|
1524
|
-
throw new Error(
|
|
1543
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1525
1544
|
const entity = await this.resolveChat(chatId);
|
|
1526
1545
|
if (entity instanceof Api.Channel) {
|
|
1527
1546
|
await this.client.invoke(new Api.channels.LeaveChannel({ channel: entity }));
|
|
@@ -1538,7 +1557,7 @@ export class TelegramService {
|
|
|
1538
1557
|
}
|
|
1539
1558
|
async setAdmin(chatId, userId, options) {
|
|
1540
1559
|
if (!this.client)
|
|
1541
|
-
throw new Error(
|
|
1560
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1542
1561
|
const entity = await this.resolveChat(chatId);
|
|
1543
1562
|
if (!(entity instanceof Api.Channel))
|
|
1544
1563
|
throw new Error("Set admin is only supported for supergroups and channels");
|
|
@@ -1564,7 +1583,7 @@ export class TelegramService {
|
|
|
1564
1583
|
}
|
|
1565
1584
|
async removeAdmin(chatId, userId) {
|
|
1566
1585
|
if (!this.client)
|
|
1567
|
-
throw new Error(
|
|
1586
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1568
1587
|
const entity = await this.resolveChat(chatId);
|
|
1569
1588
|
if (!(entity instanceof Api.Channel))
|
|
1570
1589
|
throw new Error("Remove admin is only supported for supergroups and channels");
|
package/dist/tools/messages.js
CHANGED
|
@@ -16,9 +16,11 @@ export function registerMessageTools(server, telegram) {
|
|
|
16
16
|
if (err)
|
|
17
17
|
return fail(new Error(err));
|
|
18
18
|
try {
|
|
19
|
-
await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
|
|
19
|
+
const result = await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
|
|
20
20
|
const dest = topicId ? `topic ${topicId} in ${chatId}` : chatId;
|
|
21
|
-
|
|
21
|
+
const messageId = result?.id;
|
|
22
|
+
const idInfo = messageId ? ` [#${messageId}]` : "";
|
|
23
|
+
return ok(`Message sent to ${dest}${idInfo}`);
|
|
22
24
|
}
|
|
23
25
|
catch (e) {
|
|
24
26
|
return fail(e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@overpod/mcp-telegram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "MCP server for Telegram userbot — messages, media, reactions, polls & more. Built on GramJS/MTProto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
16
|
"README.md",
|
|
17
|
-
"LICENSE"
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"CHANGELOG.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"dev": "tsx watch src/index.ts",
|