@overpod/mcp-telegram 1.19.0 → 1.20.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 +273 -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 +3 -1
- package/dist/telegram-client.js +86 -70
- package/dist/tools/messages.js +4 -2
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
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.20.0] - 2026-03-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Rate limiting & retry** — automatic FLOOD_WAIT handling, network error recovery with exponential backoff (`src/rate-limiter.ts`)
|
|
14
|
+
- `send-message` now returns `messageId` in the response (`Message sent to @user [#12345]`), enabling send → edit workflows (closes #16)
|
|
15
|
+
- Rate limiter unit tests (7 tests in `src/__tests__/rate-limiter.test.ts`)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `sendMessage()` return type changed from `void` to `Api.Message | Api.UpdateShortSentMessage | undefined`
|
|
19
|
+
- Write methods (`sendMessage`, `sendFile`, `editMessage`, `deleteMessages`) are now rate-limited with automatic retry on transient errors
|
|
20
|
+
|
|
21
|
+
## [1.19.0] - 2026-03-30
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Docker support for containerized deployment
|
|
25
|
+
- Non-blocking startup behavior
|
|
26
|
+
- Local QR code fallback for authentication
|
|
27
|
+
- Automated test infrastructure with Node.js test runner
|
|
28
|
+
- CI workflow to publish Docker images to GitHub Container Registry
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Added pnpm-lock.yaml for better dependency management
|
|
32
|
+
|
|
33
|
+
## [1.18.0] - 2026-03-28
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- New `telegram-get-my-role` tool to check user's role in a chat
|
|
37
|
+
- Role information in `telegram-get-chat-members` results
|
|
38
|
+
|
|
39
|
+
## [1.17.0] - 2026-03-28
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- Chat resolution by display name (not just ID or username)
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Updated documentation to replace static tool list with auto-discovery note
|
|
46
|
+
- Improved project structure documentation
|
|
47
|
+
|
|
48
|
+
## [1.16.0] - 2026-03-28
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- Group management tools: invite, kick, ban, edit, leave
|
|
52
|
+
- Admin management capabilities
|
|
53
|
+
|
|
54
|
+
## [1.15.0] - 2026-03-28
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- `telegram-create-group` tool for creating new groups
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- Documented `AUTH_KEY_DUPLICATED` error handling
|
|
61
|
+
|
|
62
|
+
## [1.14.0] - 2026-03-28
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
- SOCKS5 proxy support for Telegram connections
|
|
66
|
+
- MTProxy support for Telegram connections
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- Updated Biome to 2.4.9 with new config schema
|
|
70
|
+
- Sorted imports for Biome compliance
|
|
71
|
+
- Added proxy documentation to README
|
|
72
|
+
|
|
73
|
+
## [1.13.0] - 2026-03-26
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
- Refactored tools into modular files organized by category
|
|
77
|
+
|
|
78
|
+
## [1.12.0] - 2026-03-26
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- Migrated to `registerTool()` API with tool annotations
|
|
82
|
+
|
|
83
|
+
## [1.11.1] - 2026-03-25
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- Sanitized unpaired UTF-16 surrogates in tool responses
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- Upgraded TypeScript to 6.0
|
|
90
|
+
- Updated README with missing tools
|
|
91
|
+
|
|
92
|
+
## [1.11.0] - 2026-03-23
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
- Full reactions support: read, send multiple reactions, get detailed info
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
- Included message ID in all message-reading tool outputs
|
|
99
|
+
|
|
100
|
+
## [1.10.1] - 2026-03-22
|
|
101
|
+
|
|
102
|
+
### Fixed
|
|
103
|
+
- Message ID now included in all message-reading tool outputs
|
|
104
|
+
|
|
105
|
+
## [1.10.0] - 2026-03-20
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
- Enhanced `telegram-get-profile` with birthday, business, and premium data
|
|
109
|
+
- New `telegram-get-profile-photo` tool
|
|
110
|
+
- Global message search capability
|
|
111
|
+
- Enriched chat search results
|
|
112
|
+
|
|
113
|
+
## [1.9.0] - 2026-03-18
|
|
114
|
+
|
|
115
|
+
### Added
|
|
116
|
+
- Forum Topics support
|
|
117
|
+
- Per-topic unread count for forum groups
|
|
118
|
+
- Secure session storage with configurable path
|
|
119
|
+
- Multiple accounts support
|
|
120
|
+
|
|
121
|
+
### Fixed
|
|
122
|
+
- Per-topic unread sum calculation for forum groups
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
- Updated session path and security documentation
|
|
126
|
+
- Upgraded GitHub Actions to v6
|
|
127
|
+
- Replaced Node 20 with Node 24 in CI
|
|
128
|
+
- Updated Biome to 2.4.7 and @types/node to 25.5.0
|
|
129
|
+
|
|
130
|
+
## [1.8.1] - 2026-03-19
|
|
131
|
+
|
|
132
|
+
### Fixed
|
|
133
|
+
- Redirected console.log to stderr to prevent MCP JSON-RPC corruption
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
- Updated dependencies (Biome 2.4.8)
|
|
137
|
+
|
|
138
|
+
## [1.8.0] - 2026-03-18
|
|
139
|
+
|
|
140
|
+
### Added
|
|
141
|
+
- Secure session storage with configurable path via SESSION_PATH environment variable
|
|
142
|
+
|
|
143
|
+
### Changed
|
|
144
|
+
- Updated session path and security information in README
|
|
145
|
+
|
|
146
|
+
## [1.7.0] - 2026-03-16
|
|
147
|
+
|
|
148
|
+
### Added
|
|
149
|
+
- CI workflow to publish to GitHub Packages alongside npm
|
|
150
|
+
- Manual workflow dispatch trigger for publishing
|
|
151
|
+
|
|
152
|
+
## [1.6.0] - 2026-03-16
|
|
153
|
+
|
|
154
|
+
### Added
|
|
155
|
+
- Contact request management
|
|
156
|
+
- Block/unblock users
|
|
157
|
+
- Report spam functionality
|
|
158
|
+
- Add contact tool
|
|
159
|
+
- ChatGPT to list of supported clients
|
|
160
|
+
|
|
161
|
+
### Changed
|
|
162
|
+
- Removed hardcoded tool counts from README and package.json
|
|
163
|
+
- Updated Biome to 2.4.7 and @types/node to 25.5.0
|
|
164
|
+
|
|
165
|
+
## [1.5.0] - 2026-03-16
|
|
166
|
+
|
|
167
|
+
### Added
|
|
168
|
+
- Reactions support
|
|
169
|
+
- Scheduled messages
|
|
170
|
+
- Polls creation and management
|
|
171
|
+
- `telegram-join-chat` tool for joining groups and channels
|
|
172
|
+
|
|
173
|
+
### Changed
|
|
174
|
+
- Updated README with new tool documentation
|
|
175
|
+
- Increased tool count to 24
|
|
176
|
+
|
|
177
|
+
## [1.4.0] - 2026-03-15
|
|
178
|
+
|
|
179
|
+
### Added
|
|
180
|
+
- Glama.ai MCP catalog verification (glama.json)
|
|
181
|
+
- Smithery MCP catalog listing (smithery.yaml)
|
|
182
|
+
- Demo GIF and badges to README
|
|
183
|
+
- Hosted version link
|
|
184
|
+
|
|
185
|
+
### Fixed
|
|
186
|
+
- Removed PNG file save from CLI QR login
|
|
187
|
+
|
|
188
|
+
### Changed
|
|
189
|
+
- Updated README with Glama MCP server badge
|
|
190
|
+
|
|
191
|
+
## [1.3.1] - 2026-03-12
|
|
192
|
+
|
|
193
|
+
### Fixed
|
|
194
|
+
- Use `destroy()` instead of `disconnect()` to stop GramJS update loop
|
|
195
|
+
- Adopt QR login client directly instead of destroy+reconnect flow
|
|
196
|
+
- Destroy GramJS client in `logOut()` and `startQrLogin()` to stop update loop
|
|
197
|
+
|
|
198
|
+
## [1.3.0] - 2026-03-12
|
|
199
|
+
|
|
200
|
+
### Added
|
|
201
|
+
- `logOut()` method for complete Telegram session termination
|
|
202
|
+
|
|
203
|
+
## [1.2.0] - 2026-03-11
|
|
204
|
+
|
|
205
|
+
### Added
|
|
206
|
+
- `downloadMediaAsBuffer` for serverless media download
|
|
207
|
+
- Library exports and declaration types
|
|
208
|
+
- Date filters for messages
|
|
209
|
+
- Comprehensive README for v1.0
|
|
210
|
+
|
|
211
|
+
### Fixed
|
|
212
|
+
- MIME type detection from magic bytes in `downloadMediaAsBuffer`
|
|
213
|
+
- Made `saveSession` resilient to file write errors in Docker
|
|
214
|
+
|
|
215
|
+
### Changed
|
|
216
|
+
- Use `GetFullChannel`/`GetFullChat` for complete chat information
|
|
217
|
+
- Improved `telegram-login` for Claude Desktop users
|
|
218
|
+
- Added npm publishing support and GitHub Actions CI/CD
|
|
219
|
+
|
|
220
|
+
## [1.1.0] - 2026-03-11
|
|
221
|
+
|
|
222
|
+
### Added
|
|
223
|
+
- Contact management tools
|
|
224
|
+
- Chat members listing
|
|
225
|
+
- User profile retrieval
|
|
226
|
+
- Chat type filter
|
|
227
|
+
- Media tools (send, download, get info)
|
|
228
|
+
- Pin/unpin messages
|
|
229
|
+
- Markdown support
|
|
230
|
+
- Media information in messages
|
|
231
|
+
- Unread counts
|
|
232
|
+
- Mark messages as read
|
|
233
|
+
- Forward messages
|
|
234
|
+
- Edit messages
|
|
235
|
+
- Delete messages
|
|
236
|
+
- Detailed chat information
|
|
237
|
+
- Pagination support
|
|
238
|
+
|
|
239
|
+
## [1.0.0] - 2026-03-10
|
|
240
|
+
|
|
241
|
+
### Added
|
|
242
|
+
- Initial release: MCP server for Telegram userbot
|
|
243
|
+
- Basic message reading and sending
|
|
244
|
+
- Chat listing
|
|
245
|
+
- Authentication via phone number and QR code
|
|
246
|
+
- Session persistence
|
|
247
|
+
- GramJS/MTProto integration
|
|
248
|
+
|
|
249
|
+
[Unreleased]: https://github.com/overpod/mcp-telegram/compare/v1.19.0...HEAD
|
|
250
|
+
[1.19.0]: https://github.com/overpod/mcp-telegram/compare/v1.18.0...v1.19.0
|
|
251
|
+
[1.18.0]: https://github.com/overpod/mcp-telegram/compare/v1.17.0...v1.18.0
|
|
252
|
+
[1.17.0]: https://github.com/overpod/mcp-telegram/compare/v1.16.0...v1.17.0
|
|
253
|
+
[1.16.0]: https://github.com/overpod/mcp-telegram/compare/v1.15.0...v1.16.0
|
|
254
|
+
[1.15.0]: https://github.com/overpod/mcp-telegram/compare/v1.14.0...v1.15.0
|
|
255
|
+
[1.14.0]: https://github.com/overpod/mcp-telegram/compare/v1.13.0...v1.14.0
|
|
256
|
+
[1.13.0]: https://github.com/overpod/mcp-telegram/compare/v1.12.0...v1.13.0
|
|
257
|
+
[1.12.0]: https://github.com/overpod/mcp-telegram/compare/v1.11.1...v1.12.0
|
|
258
|
+
[1.11.1]: https://github.com/overpod/mcp-telegram/compare/v1.11.0...v1.11.1
|
|
259
|
+
[1.11.0]: https://github.com/overpod/mcp-telegram/compare/v1.10.1...v1.11.0
|
|
260
|
+
[1.10.1]: https://github.com/overpod/mcp-telegram/compare/v1.10.0...v1.10.1
|
|
261
|
+
[1.10.0]: https://github.com/overpod/mcp-telegram/compare/v1.9.0...v1.10.0
|
|
262
|
+
[1.9.0]: https://github.com/overpod/mcp-telegram/compare/v1.8.1...v1.9.0
|
|
263
|
+
[1.8.1]: https://github.com/overpod/mcp-telegram/compare/v1.8.0...v1.8.1
|
|
264
|
+
[1.8.0]: https://github.com/overpod/mcp-telegram/compare/v1.7.0...v1.8.0
|
|
265
|
+
[1.7.0]: https://github.com/overpod/mcp-telegram/compare/v1.6.0...v1.7.0
|
|
266
|
+
[1.6.0]: https://github.com/overpod/mcp-telegram/compare/v1.5.0...v1.6.0
|
|
267
|
+
[1.5.0]: https://github.com/overpod/mcp-telegram/compare/v1.4.0...v1.5.0
|
|
268
|
+
[1.4.0]: https://github.com/overpod/mcp-telegram/compare/v1.3.1...v1.4.0
|
|
269
|
+
[1.3.1]: https://github.com/overpod/mcp-telegram/compare/v1.3.0...v1.3.1
|
|
270
|
+
[1.3.0]: https://github.com/overpod/mcp-telegram/compare/v1.2.0...v1.3.0
|
|
271
|
+
[1.2.0]: https://github.com/overpod/mcp-telegram/compare/v1.1.0...v1.2.0
|
|
272
|
+
[1.1.0]: https://github.com/overpod/mcp-telegram/compare/v1.0.0...v1.1.0
|
|
273
|
+
[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,4 @@
|
|
|
1
|
+
import { Api } from "telegram/tl/index.js";
|
|
1
2
|
export declare class TelegramService {
|
|
2
3
|
private client;
|
|
3
4
|
private apiId;
|
|
@@ -5,6 +6,7 @@ export declare class TelegramService {
|
|
|
5
6
|
private sessionString;
|
|
6
7
|
private connected;
|
|
7
8
|
private sessionPath;
|
|
9
|
+
private rateLimiter;
|
|
8
10
|
lastError: string;
|
|
9
11
|
get sessionDir(): string;
|
|
10
12
|
constructor(apiId: number, apiHash: string, options?: {
|
|
@@ -37,7 +39,7 @@ export declare class TelegramService {
|
|
|
37
39
|
username?: string;
|
|
38
40
|
firstName?: string;
|
|
39
41
|
}>;
|
|
40
|
-
sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<
|
|
42
|
+
sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<Api.Message | Api.UpdateShortSentMessage | undefined>;
|
|
41
43
|
sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
|
|
42
44
|
downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
|
|
43
45
|
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,6 +51,7 @@ 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);
|
|
@@ -136,7 +139,7 @@ export class TelegramService {
|
|
|
136
139
|
// Auth revoked — delete invalid session
|
|
137
140
|
if (msg === "AUTH_KEY_UNREGISTERED" || msg === "SESSION_REVOKED" || msg === "USER_DEACTIVATED") {
|
|
138
141
|
await this.clearSession();
|
|
139
|
-
this.lastError = "Session revoked.
|
|
142
|
+
this.lastError = "Session revoked. Run telegram-login to re-authenticate.";
|
|
140
143
|
}
|
|
141
144
|
// Network error — keep session, just report
|
|
142
145
|
else if (msg.includes("TIMEOUT") ||
|
|
@@ -144,7 +147,7 @@ export class TelegramService {
|
|
|
144
147
|
msg.includes("ENETUNREACH") ||
|
|
145
148
|
msg.includes("ENOTFOUND") ||
|
|
146
149
|
msg.includes("network")) {
|
|
147
|
-
this.lastError = `Network error: ${msg}.
|
|
150
|
+
this.lastError = `Network error: ${msg}. Run telegram-status to retry connection.`;
|
|
148
151
|
}
|
|
149
152
|
// Unknown error
|
|
150
153
|
else {
|
|
@@ -290,7 +293,7 @@ export class TelegramService {
|
|
|
290
293
|
}
|
|
291
294
|
async getMe() {
|
|
292
295
|
if (!this.client || !this.connected)
|
|
293
|
-
throw new Error(
|
|
296
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
294
297
|
const me = await this.client.getMe();
|
|
295
298
|
const user = me;
|
|
296
299
|
return {
|
|
@@ -301,38 +304,47 @@ export class TelegramService {
|
|
|
301
304
|
}
|
|
302
305
|
async sendMessage(chatId, text, replyTo, parseMode, topicId) {
|
|
303
306
|
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
|
-
|
|
307
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
308
|
+
return this.rateLimiter.execute(async () => {
|
|
309
|
+
const resolved = await this.resolvePeer(chatId);
|
|
310
|
+
if (topicId) {
|
|
311
|
+
const peer = await this.client?.getInputEntity(resolved);
|
|
312
|
+
const result = await this.client?.invoke(new Api.messages.SendMessage({
|
|
313
|
+
peer,
|
|
314
|
+
message: text,
|
|
315
|
+
randomId: bigInt(Math.floor(Math.random() * 1e15)),
|
|
316
|
+
replyTo: new Api.InputReplyToMessage({
|
|
317
|
+
replyToMsgId: replyTo ?? topicId,
|
|
318
|
+
topMsgId: topicId,
|
|
319
|
+
}),
|
|
320
|
+
}));
|
|
321
|
+
if (result instanceof Api.UpdateShortSentMessage)
|
|
322
|
+
return result;
|
|
323
|
+
if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
|
|
324
|
+
const msgUpdate = result.updates.find((u) => u instanceof Api.UpdateNewMessage);
|
|
325
|
+
if (msgUpdate?.message instanceof Api.Message)
|
|
326
|
+
return msgUpdate.message;
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
return await this.client?.sendMessage(resolved, {
|
|
321
331
|
message: text,
|
|
322
332
|
...(replyTo ? { replyTo } : {}),
|
|
323
333
|
...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
|
|
324
334
|
});
|
|
325
|
-
}
|
|
335
|
+
}, `sendMessage to ${chatId}`);
|
|
326
336
|
}
|
|
327
337
|
async sendFile(chatId, filePath, caption) {
|
|
328
338
|
if (!this.client || !this.connected)
|
|
329
|
-
throw new Error(
|
|
330
|
-
|
|
331
|
-
|
|
339
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
340
|
+
await this.rateLimiter.execute(async () => {
|
|
341
|
+
const resolved = await this.resolvePeer(chatId);
|
|
342
|
+
await this.client?.sendFile(resolved, { file: filePath, caption });
|
|
343
|
+
}, `sendFile to ${chatId}`);
|
|
332
344
|
}
|
|
333
345
|
async downloadMedia(chatId, messageId, downloadPath) {
|
|
334
346
|
if (!this.client || !this.connected)
|
|
335
|
-
throw new Error(
|
|
347
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
336
348
|
const resolved = await this.resolvePeer(chatId);
|
|
337
349
|
const messages = await this.client.getMessages(resolved, { ids: [messageId] });
|
|
338
350
|
const message = messages[0];
|
|
@@ -348,7 +360,7 @@ export class TelegramService {
|
|
|
348
360
|
}
|
|
349
361
|
async downloadMediaAsBuffer(chatId, messageId) {
|
|
350
362
|
if (!this.client || !this.connected)
|
|
351
|
-
throw new Error(
|
|
363
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
352
364
|
const resolved = await this.resolvePeer(chatId);
|
|
353
365
|
const messages = await this.client.getMessages(resolved, { ids: [messageId] });
|
|
354
366
|
const message = messages[0];
|
|
@@ -384,19 +396,19 @@ export class TelegramService {
|
|
|
384
396
|
}
|
|
385
397
|
async pinMessage(chatId, messageId, silent = false) {
|
|
386
398
|
if (!this.client || !this.connected)
|
|
387
|
-
throw new Error(
|
|
399
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
388
400
|
const resolved = await this.resolvePeer(chatId);
|
|
389
401
|
await this.client.pinMessage(resolved, messageId, { notify: !silent });
|
|
390
402
|
}
|
|
391
403
|
async unpinMessage(chatId, messageId) {
|
|
392
404
|
if (!this.client || !this.connected)
|
|
393
|
-
throw new Error(
|
|
405
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
394
406
|
const resolved = await this.resolvePeer(chatId);
|
|
395
407
|
await this.client.unpinMessage(resolved, messageId);
|
|
396
408
|
}
|
|
397
409
|
async getDialogs(limit = 20, offsetDate, filterType) {
|
|
398
410
|
if (!this.client || !this.connected)
|
|
399
|
-
throw new Error(
|
|
411
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
400
412
|
const fetchLimit = filterType ? limit * 3 : limit;
|
|
401
413
|
const dialogs = await this.client.getDialogs({ limit: fetchLimit, ...(offsetDate ? { offsetDate } : {}) });
|
|
402
414
|
const mapped = dialogs.map((d) => {
|
|
@@ -419,7 +431,7 @@ export class TelegramService {
|
|
|
419
431
|
}
|
|
420
432
|
async getUnreadDialogs(limit = 20) {
|
|
421
433
|
if (!this.client || !this.connected)
|
|
422
|
-
throw new Error(
|
|
434
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
423
435
|
const dialogs = await this.client.getDialogs({ limit: limit * 3 });
|
|
424
436
|
const unread = dialogs.filter((d) => d.unreadCount > 0).slice(0, limit);
|
|
425
437
|
const results = await Promise.all(unread.map(async (d) => {
|
|
@@ -460,7 +472,7 @@ export class TelegramService {
|
|
|
460
472
|
}
|
|
461
473
|
async getContactRequests(limit = 20) {
|
|
462
474
|
if (!this.client || !this.connected)
|
|
463
|
-
throw new Error(
|
|
475
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
464
476
|
const dialogs = await this.client.getDialogs({ limit: limit * 5 });
|
|
465
477
|
return dialogs
|
|
466
478
|
.filter((d) => {
|
|
@@ -485,7 +497,7 @@ export class TelegramService {
|
|
|
485
497
|
}
|
|
486
498
|
async addContact(userId, firstName, lastName, phone) {
|
|
487
499
|
if (!this.client || !this.connected)
|
|
488
|
-
throw new Error(
|
|
500
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
489
501
|
const entity = await this.client.getInputEntity(userId);
|
|
490
502
|
await this.client.invoke(new Api.contacts.AddContact({
|
|
491
503
|
id: entity,
|
|
@@ -496,39 +508,43 @@ export class TelegramService {
|
|
|
496
508
|
}
|
|
497
509
|
async blockUser(userId) {
|
|
498
510
|
if (!this.client || !this.connected)
|
|
499
|
-
throw new Error(
|
|
511
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
500
512
|
const entity = await this.client.getInputEntity(userId);
|
|
501
513
|
await this.client.invoke(new Api.contacts.Block({ id: entity }));
|
|
502
514
|
}
|
|
503
515
|
async reportSpam(chatId) {
|
|
504
516
|
if (!this.client || !this.connected)
|
|
505
|
-
throw new Error(
|
|
517
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
506
518
|
const peer = await this.client.getInputEntity(chatId);
|
|
507
519
|
await this.client.invoke(new Api.messages.ReportSpam({ peer }));
|
|
508
520
|
}
|
|
509
521
|
async markAsRead(chatId) {
|
|
510
522
|
if (!this.client || !this.connected)
|
|
511
|
-
throw new Error(
|
|
523
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
512
524
|
await this.client.markAsRead(chatId);
|
|
513
525
|
}
|
|
514
526
|
async forwardMessage(fromChatId, toChatId, messageIds) {
|
|
515
527
|
if (!this.client || !this.connected)
|
|
516
|
-
throw new Error(
|
|
528
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
517
529
|
const resolvedFrom = await this.resolvePeer(fromChatId);
|
|
518
530
|
const resolvedTo = await this.resolvePeer(toChatId);
|
|
519
531
|
await this.client.forwardMessages(resolvedTo, { messages: messageIds, fromPeer: resolvedFrom });
|
|
520
532
|
}
|
|
521
533
|
async editMessage(chatId, messageId, newText) {
|
|
522
534
|
if (!this.client || !this.connected)
|
|
523
|
-
throw new Error(
|
|
524
|
-
|
|
525
|
-
|
|
535
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
536
|
+
await this.rateLimiter.execute(async () => {
|
|
537
|
+
const resolved = await this.resolvePeer(chatId);
|
|
538
|
+
await this.client?.editMessage(resolved, { message: messageId, text: newText });
|
|
539
|
+
}, `editMessage ${messageId} in ${chatId}`);
|
|
526
540
|
}
|
|
527
541
|
async deleteMessages(chatId, messageIds) {
|
|
528
542
|
if (!this.client || !this.connected)
|
|
529
|
-
throw new Error(
|
|
530
|
-
|
|
531
|
-
|
|
543
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
544
|
+
await this.rateLimiter.execute(async () => {
|
|
545
|
+
const resolved = await this.resolvePeer(chatId);
|
|
546
|
+
await this.client?.deleteMessages(resolved, messageIds, { revoke: true });
|
|
547
|
+
}, `deleteMessages in ${chatId}`);
|
|
532
548
|
}
|
|
533
549
|
/**
|
|
534
550
|
* Resolve a chat by ID, username, or display name.
|
|
@@ -537,7 +553,7 @@ export class TelegramService {
|
|
|
537
553
|
// biome-ignore lint: GramJS has no proper entity union type
|
|
538
554
|
async resolveChat(chatId) {
|
|
539
555
|
if (!this.client)
|
|
540
|
-
throw new Error(
|
|
556
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
541
557
|
// First try direct resolve (numeric ID, username, phone)
|
|
542
558
|
try {
|
|
543
559
|
return await this.client.getEntity(chatId);
|
|
@@ -576,7 +592,7 @@ export class TelegramService {
|
|
|
576
592
|
}
|
|
577
593
|
async getChatInfo(chatId) {
|
|
578
594
|
if (!this.client || !this.connected)
|
|
579
|
-
throw new Error(
|
|
595
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
580
596
|
const entity = await this.resolveChat(chatId);
|
|
581
597
|
if (entity instanceof Api.User) {
|
|
582
598
|
const parts = [entity.firstName, entity.lastName].filter(Boolean);
|
|
@@ -684,7 +700,7 @@ export class TelegramService {
|
|
|
684
700
|
}
|
|
685
701
|
async getMessages(chatId, limit = 10, offsetId, minDate, maxDate) {
|
|
686
702
|
if (!this.client || !this.connected)
|
|
687
|
-
throw new Error(
|
|
703
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
688
704
|
const resolved = await this.resolvePeer(chatId);
|
|
689
705
|
const opts = {
|
|
690
706
|
limit,
|
|
@@ -708,7 +724,7 @@ export class TelegramService {
|
|
|
708
724
|
}
|
|
709
725
|
async searchChats(query, limit = 10) {
|
|
710
726
|
if (!this.client || !this.connected)
|
|
711
|
-
throw new Error(
|
|
727
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
712
728
|
const result = await this.client.invoke(new Api.contacts.Search({ q: query, limit }));
|
|
713
729
|
const chats = [];
|
|
714
730
|
for (const user of result.users) {
|
|
@@ -769,7 +785,7 @@ export class TelegramService {
|
|
|
769
785
|
}
|
|
770
786
|
async searchGlobal(query, limit = 20, minDate, maxDate) {
|
|
771
787
|
if (!this.client || !this.connected)
|
|
772
|
-
throw new Error(
|
|
788
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
773
789
|
const result = await this.client.invoke(new Api.messages.SearchGlobal({
|
|
774
790
|
q: query,
|
|
775
791
|
filter: new Api.InputMessagesFilterEmpty(),
|
|
@@ -838,7 +854,7 @@ export class TelegramService {
|
|
|
838
854
|
}
|
|
839
855
|
async searchMessages(chatId, query, limit = 20, minDate, maxDate) {
|
|
840
856
|
if (!this.client || !this.connected)
|
|
841
|
-
throw new Error(
|
|
857
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
842
858
|
const resolved = await this.resolvePeer(chatId);
|
|
843
859
|
const messages = await this.client.getMessages(resolved, {
|
|
844
860
|
search: query,
|
|
@@ -861,7 +877,7 @@ export class TelegramService {
|
|
|
861
877
|
}
|
|
862
878
|
async getContacts(limit = 50) {
|
|
863
879
|
if (!this.client || !this.connected)
|
|
864
|
-
throw new Error(
|
|
880
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
865
881
|
const result = await this.client.invoke(new Api.contacts.GetContacts({ hash: bigInt(0) }));
|
|
866
882
|
if (!(result instanceof Api.contacts.Contacts))
|
|
867
883
|
return [];
|
|
@@ -881,7 +897,7 @@ export class TelegramService {
|
|
|
881
897
|
}
|
|
882
898
|
async getChatMembers(chatId, limit = 50) {
|
|
883
899
|
if (!this.client || !this.connected)
|
|
884
|
-
throw new Error(
|
|
900
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
885
901
|
const entity = await this.resolveChat(chatId);
|
|
886
902
|
if (entity instanceof Api.Channel) {
|
|
887
903
|
const result = await this.client.invoke(new Api.channels.GetParticipants({
|
|
@@ -926,7 +942,7 @@ export class TelegramService {
|
|
|
926
942
|
}
|
|
927
943
|
async getMyRole(chatId) {
|
|
928
944
|
if (!this.client || !this.connected)
|
|
929
|
-
throw new Error(
|
|
945
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
930
946
|
const entity = await this.resolveChat(chatId);
|
|
931
947
|
const me = await this.getMe();
|
|
932
948
|
if (entity instanceof Api.Channel) {
|
|
@@ -978,7 +994,7 @@ export class TelegramService {
|
|
|
978
994
|
}
|
|
979
995
|
async getProfile(userId) {
|
|
980
996
|
if (!this.client || !this.connected)
|
|
981
|
-
throw new Error(
|
|
997
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
982
998
|
const entity = await this.client.getEntity(userId);
|
|
983
999
|
if (!(entity instanceof Api.User))
|
|
984
1000
|
throw new Error("Entity is not a user");
|
|
@@ -1038,7 +1054,7 @@ export class TelegramService {
|
|
|
1038
1054
|
}
|
|
1039
1055
|
async downloadProfilePhoto(entityId, options) {
|
|
1040
1056
|
if (!this.client || !this.connected)
|
|
1041
|
-
throw new Error(
|
|
1057
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1042
1058
|
const entity = await this.client.getEntity(entityId);
|
|
1043
1059
|
const buffer = (await this.client.downloadProfilePhoto(entity, {
|
|
1044
1060
|
isBig: options?.isBig !== false,
|
|
@@ -1089,7 +1105,7 @@ export class TelegramService {
|
|
|
1089
1105
|
}
|
|
1090
1106
|
async sendReaction(chatId, messageId, emoji, addToExisting = false) {
|
|
1091
1107
|
if (!this.client || !this.connected)
|
|
1092
|
-
throw new Error(
|
|
1108
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1093
1109
|
const resolved = await this.resolvePeer(chatId);
|
|
1094
1110
|
const peer = await this.client.getInputEntity(resolved);
|
|
1095
1111
|
const reactionList = [];
|
|
@@ -1129,7 +1145,7 @@ export class TelegramService {
|
|
|
1129
1145
|
}
|
|
1130
1146
|
async getMessageReactions(chatId, messageId) {
|
|
1131
1147
|
if (!this.client || !this.connected)
|
|
1132
|
-
throw new Error(
|
|
1148
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1133
1149
|
const resolved = await this.resolvePeer(chatId);
|
|
1134
1150
|
const peer = await this.client.getInputEntity(resolved);
|
|
1135
1151
|
// First get the message to know which reactions exist
|
|
@@ -1184,7 +1200,7 @@ export class TelegramService {
|
|
|
1184
1200
|
}
|
|
1185
1201
|
async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
|
|
1186
1202
|
if (!this.client || !this.connected)
|
|
1187
|
-
throw new Error(
|
|
1203
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1188
1204
|
const resolved = await this.resolvePeer(chatId);
|
|
1189
1205
|
await this.client.sendMessage(resolved, {
|
|
1190
1206
|
message: text,
|
|
@@ -1195,7 +1211,7 @@ export class TelegramService {
|
|
|
1195
1211
|
}
|
|
1196
1212
|
async createPoll(chatId, question, answers, options) {
|
|
1197
1213
|
if (!this.client || !this.connected)
|
|
1198
|
-
throw new Error(
|
|
1214
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1199
1215
|
const peer = await this.client.getInputEntity(chatId);
|
|
1200
1216
|
const pollAnswers = answers.map((text, i) => new Api.PollAnswer({
|
|
1201
1217
|
text: new Api.TextWithEntities({ text, entities: [] }),
|
|
@@ -1231,7 +1247,7 @@ export class TelegramService {
|
|
|
1231
1247
|
}
|
|
1232
1248
|
async getForumTopics(chatId, limit = 100) {
|
|
1233
1249
|
if (!this.client || !this.connected)
|
|
1234
|
-
throw new Error(
|
|
1250
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1235
1251
|
const entity = await this.resolveChat(chatId);
|
|
1236
1252
|
if (!(entity instanceof Api.Channel))
|
|
1237
1253
|
throw new Error("Forum topics are only available in supergroups");
|
|
@@ -1260,7 +1276,7 @@ export class TelegramService {
|
|
|
1260
1276
|
}
|
|
1261
1277
|
async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
|
|
1262
1278
|
if (!this.client || !this.connected)
|
|
1263
|
-
throw new Error(
|
|
1279
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1264
1280
|
const peer = await this.client.getInputEntity(chatId);
|
|
1265
1281
|
const result = await this.client.invoke(new Api.messages.GetReplies({
|
|
1266
1282
|
peer,
|
|
@@ -1289,7 +1305,7 @@ export class TelegramService {
|
|
|
1289
1305
|
/** Check if a chat entity is a forum (has topics enabled) */
|
|
1290
1306
|
async isForum(chatId) {
|
|
1291
1307
|
if (!this.client || !this.connected)
|
|
1292
|
-
throw new Error(
|
|
1308
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1293
1309
|
try {
|
|
1294
1310
|
const entity = await this.resolveChat(chatId);
|
|
1295
1311
|
if (entity instanceof Api.Channel) {
|
|
@@ -1301,7 +1317,7 @@ export class TelegramService {
|
|
|
1301
1317
|
}
|
|
1302
1318
|
async joinChat(target) {
|
|
1303
1319
|
if (!this.client)
|
|
1304
|
-
throw new Error(
|
|
1320
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1305
1321
|
// Extract invite hash from various link formats
|
|
1306
1322
|
const inviteMatch = target.match(/(?:t\.me\/\+|t\.me\/joinchat\/|tg:\/\/join\?invite=)([a-zA-Z0-9_-]+)/);
|
|
1307
1323
|
if (inviteMatch) {
|
|
@@ -1332,7 +1348,7 @@ export class TelegramService {
|
|
|
1332
1348
|
}
|
|
1333
1349
|
async createGroup(options) {
|
|
1334
1350
|
if (!this.client)
|
|
1335
|
-
throw new Error(
|
|
1351
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1336
1352
|
const { title, users, supergroup = false, forum = false, description } = options;
|
|
1337
1353
|
if (supergroup || forum) {
|
|
1338
1354
|
// Create supergroup/channel via channels.CreateChannel
|
|
@@ -1406,7 +1422,7 @@ export class TelegramService {
|
|
|
1406
1422
|
}
|
|
1407
1423
|
async inviteToGroup(chatId, users) {
|
|
1408
1424
|
if (!this.client)
|
|
1409
|
-
throw new Error(
|
|
1425
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1410
1426
|
const entity = await this.resolveChat(chatId);
|
|
1411
1427
|
const invited = [];
|
|
1412
1428
|
const failed = [];
|
|
@@ -1434,7 +1450,7 @@ export class TelegramService {
|
|
|
1434
1450
|
}
|
|
1435
1451
|
async kickUser(chatId, userId) {
|
|
1436
1452
|
if (!this.client)
|
|
1437
|
-
throw new Error(
|
|
1453
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1438
1454
|
const entity = await this.resolveChat(chatId);
|
|
1439
1455
|
const user = await this.client.getEntity(userId);
|
|
1440
1456
|
if (!(user instanceof Api.User))
|
|
@@ -1459,7 +1475,7 @@ export class TelegramService {
|
|
|
1459
1475
|
}
|
|
1460
1476
|
async banUser(chatId, userId) {
|
|
1461
1477
|
if (!this.client)
|
|
1462
|
-
throw new Error(
|
|
1478
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1463
1479
|
const entity = await this.resolveChat(chatId);
|
|
1464
1480
|
const user = await this.client.getEntity(userId);
|
|
1465
1481
|
if (!(user instanceof Api.User))
|
|
@@ -1475,7 +1491,7 @@ export class TelegramService {
|
|
|
1475
1491
|
}
|
|
1476
1492
|
async unbanUser(chatId, userId) {
|
|
1477
1493
|
if (!this.client)
|
|
1478
|
-
throw new Error(
|
|
1494
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1479
1495
|
const entity = await this.resolveChat(chatId);
|
|
1480
1496
|
const user = await this.client.getEntity(userId);
|
|
1481
1497
|
if (!(user instanceof Api.User))
|
|
@@ -1491,7 +1507,7 @@ export class TelegramService {
|
|
|
1491
1507
|
}
|
|
1492
1508
|
async editGroup(chatId, options) {
|
|
1493
1509
|
if (!this.client)
|
|
1494
|
-
throw new Error(
|
|
1510
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1495
1511
|
const entity = await this.resolveChat(chatId);
|
|
1496
1512
|
if (options.title) {
|
|
1497
1513
|
if (entity instanceof Api.Channel) {
|
|
@@ -1521,7 +1537,7 @@ export class TelegramService {
|
|
|
1521
1537
|
}
|
|
1522
1538
|
async leaveGroup(chatId) {
|
|
1523
1539
|
if (!this.client)
|
|
1524
|
-
throw new Error(
|
|
1540
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1525
1541
|
const entity = await this.resolveChat(chatId);
|
|
1526
1542
|
if (entity instanceof Api.Channel) {
|
|
1527
1543
|
await this.client.invoke(new Api.channels.LeaveChannel({ channel: entity }));
|
|
@@ -1538,7 +1554,7 @@ export class TelegramService {
|
|
|
1538
1554
|
}
|
|
1539
1555
|
async setAdmin(chatId, userId, options) {
|
|
1540
1556
|
if (!this.client)
|
|
1541
|
-
throw new Error(
|
|
1557
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1542
1558
|
const entity = await this.resolveChat(chatId);
|
|
1543
1559
|
if (!(entity instanceof Api.Channel))
|
|
1544
1560
|
throw new Error("Set admin is only supported for supergroups and channels");
|
|
@@ -1564,7 +1580,7 @@ export class TelegramService {
|
|
|
1564
1580
|
}
|
|
1565
1581
|
async removeAdmin(chatId, userId) {
|
|
1566
1582
|
if (!this.client)
|
|
1567
|
-
throw new Error(
|
|
1583
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1568
1584
|
const entity = await this.resolveChat(chatId);
|
|
1569
1585
|
if (!(entity instanceof Api.Channel))
|
|
1570
1586
|
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.20.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",
|