@overpod/mcp-telegram 1.18.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/README.md +56 -1
- package/dist/__tests__/rate-limiter.test.d.ts +1 -0
- package/dist/__tests__/rate-limiter.test.js +81 -0
- package/dist/__tests__/tools/shared.test.d.ts +1 -0
- package/dist/__tests__/tools/shared.test.js +110 -0
- package/dist/index.js +13 -10
- package/dist/rate-limiter.d.ts +26 -0
- package/dist/rate-limiter.js +80 -0
- package/dist/telegram-client.d.ts +4 -1
- package/dist/telegram-client.js +89 -70
- package/dist/tools/auth.js +7 -11
- package/dist/tools/messages.js +4 -2
- package/package.json +6 -3
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
|
package/README.md
CHANGED
|
@@ -138,6 +138,34 @@ cd mcp-telegram
|
|
|
138
138
|
npm install && npm run build
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
### Docker
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
docker build -t mcp-telegram https://github.com/overpod/mcp-telegram.git
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Login (interactive terminal required):
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
docker run -it --rm \
|
|
151
|
+
-e TELEGRAM_API_ID=YOUR_ID \
|
|
152
|
+
-e TELEGRAM_API_HASH=YOUR_HASH \
|
|
153
|
+
-v ~/.mcp-telegram:/root/.mcp-telegram \
|
|
154
|
+
--entrypoint node mcp-telegram dist/qr-login-cli.js
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Run the MCP server:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
docker run -i --rm \
|
|
161
|
+
-e TELEGRAM_API_ID=YOUR_ID \
|
|
162
|
+
-e TELEGRAM_API_HASH=YOUR_HASH \
|
|
163
|
+
-v ~/.mcp-telegram:/root/.mcp-telegram \
|
|
164
|
+
mcp-telegram
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
> **Note**: Login must be done once via terminal. After that, the session is persisted in `~/.mcp-telegram` and reused automatically.
|
|
168
|
+
|
|
141
169
|
## Usage with MCP Clients
|
|
142
170
|
|
|
143
171
|
### Claude Code (CLI)
|
|
@@ -174,12 +202,37 @@ claude mcp add telegram -s user \
|
|
|
174
202
|
|
|
175
203
|
3. Restart Claude Desktop.
|
|
176
204
|
|
|
177
|
-
4. Ask Claude: **"Run telegram-login"** -- a QR code will appear. If the image is not visible,
|
|
205
|
+
4. Ask Claude: **"Run telegram-login"** -- a QR code will appear. If the image is not visible, it's also saved to `~/.mcp-telegram/qr-login.png`. Scan it in Telegram (**Settings > Devices > Link Desktop Device**).
|
|
178
206
|
|
|
179
207
|
5. Ask Claude: **"Run telegram-status"** to verify the connection.
|
|
180
208
|
|
|
181
209
|
> **Note**: No terminal required! Login works entirely through Claude Desktop.
|
|
182
210
|
|
|
211
|
+
### Claude Desktop (Docker)
|
|
212
|
+
|
|
213
|
+
1. Login via terminal first (see [Docker](#docker) section above).
|
|
214
|
+
|
|
215
|
+
2. Add to your config file:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"mcpServers": {
|
|
220
|
+
"telegram": {
|
|
221
|
+
"command": "docker",
|
|
222
|
+
"args": [
|
|
223
|
+
"run", "-i", "--rm",
|
|
224
|
+
"-e", "TELEGRAM_API_ID=YOUR_ID",
|
|
225
|
+
"-e", "TELEGRAM_API_HASH=YOUR_HASH",
|
|
226
|
+
"-v", "~/.mcp-telegram:/root/.mcp-telegram",
|
|
227
|
+
"mcp-telegram"
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
3. Restart Claude Desktop. Ask Claude: **"Run telegram-status"** to verify.
|
|
235
|
+
|
|
183
236
|
### Cursor / VS Code
|
|
184
237
|
|
|
185
238
|
Add the same JSON config above to your MCP settings (Cursor Settings > MCP, or VS Code MCP config).
|
|
@@ -276,6 +329,8 @@ Then set `TELEGRAM_SESSION_PATH` in each environment's MCP config accordingly.
|
|
|
276
329
|
- Session is stored in `~/.mcp-telegram/session` with `0600` permissions (owner-only access)
|
|
277
330
|
- Session directory is created with `0700` permissions
|
|
278
331
|
- Phone number is **not required** -- QR-only authentication
|
|
332
|
+
- No data is sent to third-party services -- all communication goes directly to Telegram servers via MTProto
|
|
333
|
+
- QR login codes are generated locally and never leave your machine
|
|
279
334
|
- **One session per process** -- using the same session in multiple processes simultaneously causes `AUTH_KEY_DUPLICATED` errors (see [Troubleshooting](#troubleshooting))
|
|
280
335
|
- This is a **userbot** (personal account), not a bot -- respect the [Telegram Terms of Service](https://core.telegram.org/api/terms)
|
|
281
336
|
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { DESTRUCTIVE, fail, formatReactions, ok, READ_ONLY, sanitize, WRITE } from "../../tools/shared.js";
|
|
4
|
+
describe("shared utilities", () => {
|
|
5
|
+
describe("ok()", () => {
|
|
6
|
+
it("should return success response with text content", () => {
|
|
7
|
+
const result = ok("Operation successful");
|
|
8
|
+
assert.deepStrictEqual(result, {
|
|
9
|
+
content: [{ type: "text", text: "Operation successful" }],
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
it("should handle empty string", () => {
|
|
13
|
+
const result = ok("");
|
|
14
|
+
assert.deepStrictEqual(result, {
|
|
15
|
+
content: [{ type: "text", text: "" }],
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe("fail()", () => {
|
|
20
|
+
it("should return error response with isError flag", () => {
|
|
21
|
+
const error = new Error("Something went wrong");
|
|
22
|
+
const result = fail(error);
|
|
23
|
+
assert.deepStrictEqual(result, {
|
|
24
|
+
content: [{ type: "text", text: "Error: Something went wrong" }],
|
|
25
|
+
isError: true,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it("should handle non-Error objects", () => {
|
|
29
|
+
const result = fail({ message: "Custom error" });
|
|
30
|
+
assert.ok(result.content[0].text.includes("Error:"));
|
|
31
|
+
assert.strictEqual(result.isError, true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("sanitize()", () => {
|
|
35
|
+
it("should remove unpaired high surrogates", () => {
|
|
36
|
+
const input = "Hello\uD800World";
|
|
37
|
+
const result = sanitize(input);
|
|
38
|
+
assert.strictEqual(result, "Hello\uFFFDWorld");
|
|
39
|
+
});
|
|
40
|
+
it("should remove unpaired low surrogates", () => {
|
|
41
|
+
const input = "Hello\uDC00World";
|
|
42
|
+
const result = sanitize(input);
|
|
43
|
+
assert.strictEqual(result, "Hello\uFFFDWorld");
|
|
44
|
+
});
|
|
45
|
+
it("should preserve valid surrogate pairs", () => {
|
|
46
|
+
const input = "Hello\uD83D\uDE00World"; // 😀 emoji
|
|
47
|
+
const result = sanitize(input);
|
|
48
|
+
assert.strictEqual(result, "Hello\uD83D\uDE00World");
|
|
49
|
+
});
|
|
50
|
+
it("should handle normal text without surrogates", () => {
|
|
51
|
+
const input = "Hello World";
|
|
52
|
+
const result = sanitize(input);
|
|
53
|
+
assert.strictEqual(result, "Hello World");
|
|
54
|
+
});
|
|
55
|
+
it("should handle empty string", () => {
|
|
56
|
+
const result = sanitize("");
|
|
57
|
+
assert.strictEqual(result, "");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("formatReactions()", () => {
|
|
61
|
+
it("should format reactions with counts", () => {
|
|
62
|
+
const reactions = [
|
|
63
|
+
{ emoji: "👍", count: 5, me: false },
|
|
64
|
+
{ emoji: "❤️", count: 3, me: true },
|
|
65
|
+
{ emoji: "🔥", count: 1, me: false },
|
|
66
|
+
];
|
|
67
|
+
const result = formatReactions(reactions);
|
|
68
|
+
assert.strictEqual(result, " [👍×5 ❤️×3(me) 🔥×1]");
|
|
69
|
+
});
|
|
70
|
+
it("should mark reactions from current user", () => {
|
|
71
|
+
const reactions = [{ emoji: "👍", count: 2, me: true }];
|
|
72
|
+
const result = formatReactions(reactions);
|
|
73
|
+
assert.strictEqual(result, " [👍×2(me)]");
|
|
74
|
+
});
|
|
75
|
+
it("should return empty string for undefined reactions", () => {
|
|
76
|
+
const result = formatReactions(undefined);
|
|
77
|
+
assert.strictEqual(result, "");
|
|
78
|
+
});
|
|
79
|
+
it("should return empty string for empty reactions array", () => {
|
|
80
|
+
const result = formatReactions([]);
|
|
81
|
+
assert.strictEqual(result, "");
|
|
82
|
+
});
|
|
83
|
+
it("should handle single reaction", () => {
|
|
84
|
+
const reactions = [{ emoji: "🎉", count: 1, me: false }];
|
|
85
|
+
const result = formatReactions(reactions);
|
|
86
|
+
assert.strictEqual(result, " [🎉×1]");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("MCP tool annotations", () => {
|
|
90
|
+
it("should define READ_ONLY preset", () => {
|
|
91
|
+
assert.deepStrictEqual(READ_ONLY, {
|
|
92
|
+
readOnlyHint: true,
|
|
93
|
+
openWorldHint: true,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it("should define WRITE preset", () => {
|
|
97
|
+
assert.deepStrictEqual(WRITE, {
|
|
98
|
+
readOnlyHint: false,
|
|
99
|
+
openWorldHint: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it("should define DESTRUCTIVE preset", () => {
|
|
103
|
+
assert.deepStrictEqual(DESTRUCTIVE, {
|
|
104
|
+
readOnlyHint: false,
|
|
105
|
+
destructiveHint: true,
|
|
106
|
+
openWorldHint: true,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
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);
|
|
@@ -24,18 +26,19 @@ const server = new McpServer({
|
|
|
24
26
|
});
|
|
25
27
|
registerTools(server, telegram);
|
|
26
28
|
async function main() {
|
|
27
|
-
// Try to auto-connect with saved session
|
|
28
|
-
await telegram.loadSession();
|
|
29
|
-
if (await telegram.connect()) {
|
|
30
|
-
const me = await telegram.getMe();
|
|
31
|
-
console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
|
|
32
|
-
}
|
|
33
|
-
else if (telegram.lastError) {
|
|
34
|
-
console.error(`[mcp-telegram] ${telegram.lastError}`);
|
|
35
|
-
}
|
|
36
29
|
const transport = new StdioServerTransport();
|
|
37
30
|
await server.connect(transport);
|
|
38
31
|
console.error("[mcp-telegram] MCP server running on stdio");
|
|
32
|
+
// Auto-connect with saved session after MCP is ready (non-blocking)
|
|
33
|
+
telegram.loadSession().then(async () => {
|
|
34
|
+
if (await telegram.connect()) {
|
|
35
|
+
const me = await telegram.getMe();
|
|
36
|
+
console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
|
|
37
|
+
}
|
|
38
|
+
else if (telegram.lastError) {
|
|
39
|
+
console.error(`[mcp-telegram] ${telegram.lastError}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
39
42
|
}
|
|
40
43
|
main().catch((err) => {
|
|
41
44
|
console.error("[mcp-telegram] Fatal:", err);
|
|
@@ -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,7 +6,9 @@ export declare class TelegramService {
|
|
|
5
6
|
private sessionString;
|
|
6
7
|
private connected;
|
|
7
8
|
private sessionPath;
|
|
9
|
+
private rateLimiter;
|
|
8
10
|
lastError: string;
|
|
11
|
+
get sessionDir(): string;
|
|
9
12
|
constructor(apiId: number, apiHash: string, options?: {
|
|
10
13
|
sessionPath?: string;
|
|
11
14
|
});
|
|
@@ -36,7 +39,7 @@ export declare class TelegramService {
|
|
|
36
39
|
username?: string;
|
|
37
40
|
firstName?: string;
|
|
38
41
|
}>;
|
|
39
|
-
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>;
|
|
40
43
|
sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
|
|
41
44
|
downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
|
|
42
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,7 +51,11 @@ export class TelegramService {
|
|
|
49
51
|
sessionString = "";
|
|
50
52
|
connected = false;
|
|
51
53
|
sessionPath;
|
|
54
|
+
rateLimiter = new RateLimiter();
|
|
52
55
|
lastError = "";
|
|
56
|
+
get sessionDir() {
|
|
57
|
+
return dirname(this.sessionPath);
|
|
58
|
+
}
|
|
53
59
|
constructor(apiId, apiHash, options) {
|
|
54
60
|
this.apiId = apiId;
|
|
55
61
|
this.apiHash = apiHash;
|
|
@@ -133,7 +139,7 @@ export class TelegramService {
|
|
|
133
139
|
// Auth revoked — delete invalid session
|
|
134
140
|
if (msg === "AUTH_KEY_UNREGISTERED" || msg === "SESSION_REVOKED" || msg === "USER_DEACTIVATED") {
|
|
135
141
|
await this.clearSession();
|
|
136
|
-
this.lastError = "Session revoked.
|
|
142
|
+
this.lastError = "Session revoked. Run telegram-login to re-authenticate.";
|
|
137
143
|
}
|
|
138
144
|
// Network error — keep session, just report
|
|
139
145
|
else if (msg.includes("TIMEOUT") ||
|
|
@@ -141,7 +147,7 @@ export class TelegramService {
|
|
|
141
147
|
msg.includes("ENETUNREACH") ||
|
|
142
148
|
msg.includes("ENOTFOUND") ||
|
|
143
149
|
msg.includes("network")) {
|
|
144
|
-
this.lastError = `Network error: ${msg}.
|
|
150
|
+
this.lastError = `Network error: ${msg}. Run telegram-status to retry connection.`;
|
|
145
151
|
}
|
|
146
152
|
// Unknown error
|
|
147
153
|
else {
|
|
@@ -287,7 +293,7 @@ export class TelegramService {
|
|
|
287
293
|
}
|
|
288
294
|
async getMe() {
|
|
289
295
|
if (!this.client || !this.connected)
|
|
290
|
-
throw new Error(
|
|
296
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
291
297
|
const me = await this.client.getMe();
|
|
292
298
|
const user = me;
|
|
293
299
|
return {
|
|
@@ -298,38 +304,47 @@ export class TelegramService {
|
|
|
298
304
|
}
|
|
299
305
|
async sendMessage(chatId, text, replyTo, parseMode, topicId) {
|
|
300
306
|
if (!this.client || !this.connected)
|
|
301
|
-
throw new Error(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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, {
|
|
318
331
|
message: text,
|
|
319
332
|
...(replyTo ? { replyTo } : {}),
|
|
320
333
|
...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
|
|
321
334
|
});
|
|
322
|
-
}
|
|
335
|
+
}, `sendMessage to ${chatId}`);
|
|
323
336
|
}
|
|
324
337
|
async sendFile(chatId, filePath, caption) {
|
|
325
338
|
if (!this.client || !this.connected)
|
|
326
|
-
throw new Error(
|
|
327
|
-
|
|
328
|
-
|
|
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}`);
|
|
329
344
|
}
|
|
330
345
|
async downloadMedia(chatId, messageId, downloadPath) {
|
|
331
346
|
if (!this.client || !this.connected)
|
|
332
|
-
throw new Error(
|
|
347
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
333
348
|
const resolved = await this.resolvePeer(chatId);
|
|
334
349
|
const messages = await this.client.getMessages(resolved, { ids: [messageId] });
|
|
335
350
|
const message = messages[0];
|
|
@@ -345,7 +360,7 @@ export class TelegramService {
|
|
|
345
360
|
}
|
|
346
361
|
async downloadMediaAsBuffer(chatId, messageId) {
|
|
347
362
|
if (!this.client || !this.connected)
|
|
348
|
-
throw new Error(
|
|
363
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
349
364
|
const resolved = await this.resolvePeer(chatId);
|
|
350
365
|
const messages = await this.client.getMessages(resolved, { ids: [messageId] });
|
|
351
366
|
const message = messages[0];
|
|
@@ -381,19 +396,19 @@ export class TelegramService {
|
|
|
381
396
|
}
|
|
382
397
|
async pinMessage(chatId, messageId, silent = false) {
|
|
383
398
|
if (!this.client || !this.connected)
|
|
384
|
-
throw new Error(
|
|
399
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
385
400
|
const resolved = await this.resolvePeer(chatId);
|
|
386
401
|
await this.client.pinMessage(resolved, messageId, { notify: !silent });
|
|
387
402
|
}
|
|
388
403
|
async unpinMessage(chatId, messageId) {
|
|
389
404
|
if (!this.client || !this.connected)
|
|
390
|
-
throw new Error(
|
|
405
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
391
406
|
const resolved = await this.resolvePeer(chatId);
|
|
392
407
|
await this.client.unpinMessage(resolved, messageId);
|
|
393
408
|
}
|
|
394
409
|
async getDialogs(limit = 20, offsetDate, filterType) {
|
|
395
410
|
if (!this.client || !this.connected)
|
|
396
|
-
throw new Error(
|
|
411
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
397
412
|
const fetchLimit = filterType ? limit * 3 : limit;
|
|
398
413
|
const dialogs = await this.client.getDialogs({ limit: fetchLimit, ...(offsetDate ? { offsetDate } : {}) });
|
|
399
414
|
const mapped = dialogs.map((d) => {
|
|
@@ -416,7 +431,7 @@ export class TelegramService {
|
|
|
416
431
|
}
|
|
417
432
|
async getUnreadDialogs(limit = 20) {
|
|
418
433
|
if (!this.client || !this.connected)
|
|
419
|
-
throw new Error(
|
|
434
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
420
435
|
const dialogs = await this.client.getDialogs({ limit: limit * 3 });
|
|
421
436
|
const unread = dialogs.filter((d) => d.unreadCount > 0).slice(0, limit);
|
|
422
437
|
const results = await Promise.all(unread.map(async (d) => {
|
|
@@ -457,7 +472,7 @@ export class TelegramService {
|
|
|
457
472
|
}
|
|
458
473
|
async getContactRequests(limit = 20) {
|
|
459
474
|
if (!this.client || !this.connected)
|
|
460
|
-
throw new Error(
|
|
475
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
461
476
|
const dialogs = await this.client.getDialogs({ limit: limit * 5 });
|
|
462
477
|
return dialogs
|
|
463
478
|
.filter((d) => {
|
|
@@ -482,7 +497,7 @@ export class TelegramService {
|
|
|
482
497
|
}
|
|
483
498
|
async addContact(userId, firstName, lastName, phone) {
|
|
484
499
|
if (!this.client || !this.connected)
|
|
485
|
-
throw new Error(
|
|
500
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
486
501
|
const entity = await this.client.getInputEntity(userId);
|
|
487
502
|
await this.client.invoke(new Api.contacts.AddContact({
|
|
488
503
|
id: entity,
|
|
@@ -493,39 +508,43 @@ export class TelegramService {
|
|
|
493
508
|
}
|
|
494
509
|
async blockUser(userId) {
|
|
495
510
|
if (!this.client || !this.connected)
|
|
496
|
-
throw new Error(
|
|
511
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
497
512
|
const entity = await this.client.getInputEntity(userId);
|
|
498
513
|
await this.client.invoke(new Api.contacts.Block({ id: entity }));
|
|
499
514
|
}
|
|
500
515
|
async reportSpam(chatId) {
|
|
501
516
|
if (!this.client || !this.connected)
|
|
502
|
-
throw new Error(
|
|
517
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
503
518
|
const peer = await this.client.getInputEntity(chatId);
|
|
504
519
|
await this.client.invoke(new Api.messages.ReportSpam({ peer }));
|
|
505
520
|
}
|
|
506
521
|
async markAsRead(chatId) {
|
|
507
522
|
if (!this.client || !this.connected)
|
|
508
|
-
throw new Error(
|
|
523
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
509
524
|
await this.client.markAsRead(chatId);
|
|
510
525
|
}
|
|
511
526
|
async forwardMessage(fromChatId, toChatId, messageIds) {
|
|
512
527
|
if (!this.client || !this.connected)
|
|
513
|
-
throw new Error(
|
|
528
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
514
529
|
const resolvedFrom = await this.resolvePeer(fromChatId);
|
|
515
530
|
const resolvedTo = await this.resolvePeer(toChatId);
|
|
516
531
|
await this.client.forwardMessages(resolvedTo, { messages: messageIds, fromPeer: resolvedFrom });
|
|
517
532
|
}
|
|
518
533
|
async editMessage(chatId, messageId, newText) {
|
|
519
534
|
if (!this.client || !this.connected)
|
|
520
|
-
throw new Error(
|
|
521
|
-
|
|
522
|
-
|
|
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}`);
|
|
523
540
|
}
|
|
524
541
|
async deleteMessages(chatId, messageIds) {
|
|
525
542
|
if (!this.client || !this.connected)
|
|
526
|
-
throw new Error(
|
|
527
|
-
|
|
528
|
-
|
|
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}`);
|
|
529
548
|
}
|
|
530
549
|
/**
|
|
531
550
|
* Resolve a chat by ID, username, or display name.
|
|
@@ -534,7 +553,7 @@ export class TelegramService {
|
|
|
534
553
|
// biome-ignore lint: GramJS has no proper entity union type
|
|
535
554
|
async resolveChat(chatId) {
|
|
536
555
|
if (!this.client)
|
|
537
|
-
throw new Error(
|
|
556
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
538
557
|
// First try direct resolve (numeric ID, username, phone)
|
|
539
558
|
try {
|
|
540
559
|
return await this.client.getEntity(chatId);
|
|
@@ -573,7 +592,7 @@ export class TelegramService {
|
|
|
573
592
|
}
|
|
574
593
|
async getChatInfo(chatId) {
|
|
575
594
|
if (!this.client || !this.connected)
|
|
576
|
-
throw new Error(
|
|
595
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
577
596
|
const entity = await this.resolveChat(chatId);
|
|
578
597
|
if (entity instanceof Api.User) {
|
|
579
598
|
const parts = [entity.firstName, entity.lastName].filter(Boolean);
|
|
@@ -681,7 +700,7 @@ export class TelegramService {
|
|
|
681
700
|
}
|
|
682
701
|
async getMessages(chatId, limit = 10, offsetId, minDate, maxDate) {
|
|
683
702
|
if (!this.client || !this.connected)
|
|
684
|
-
throw new Error(
|
|
703
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
685
704
|
const resolved = await this.resolvePeer(chatId);
|
|
686
705
|
const opts = {
|
|
687
706
|
limit,
|
|
@@ -705,7 +724,7 @@ export class TelegramService {
|
|
|
705
724
|
}
|
|
706
725
|
async searchChats(query, limit = 10) {
|
|
707
726
|
if (!this.client || !this.connected)
|
|
708
|
-
throw new Error(
|
|
727
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
709
728
|
const result = await this.client.invoke(new Api.contacts.Search({ q: query, limit }));
|
|
710
729
|
const chats = [];
|
|
711
730
|
for (const user of result.users) {
|
|
@@ -766,7 +785,7 @@ export class TelegramService {
|
|
|
766
785
|
}
|
|
767
786
|
async searchGlobal(query, limit = 20, minDate, maxDate) {
|
|
768
787
|
if (!this.client || !this.connected)
|
|
769
|
-
throw new Error(
|
|
788
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
770
789
|
const result = await this.client.invoke(new Api.messages.SearchGlobal({
|
|
771
790
|
q: query,
|
|
772
791
|
filter: new Api.InputMessagesFilterEmpty(),
|
|
@@ -835,7 +854,7 @@ export class TelegramService {
|
|
|
835
854
|
}
|
|
836
855
|
async searchMessages(chatId, query, limit = 20, minDate, maxDate) {
|
|
837
856
|
if (!this.client || !this.connected)
|
|
838
|
-
throw new Error(
|
|
857
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
839
858
|
const resolved = await this.resolvePeer(chatId);
|
|
840
859
|
const messages = await this.client.getMessages(resolved, {
|
|
841
860
|
search: query,
|
|
@@ -858,7 +877,7 @@ export class TelegramService {
|
|
|
858
877
|
}
|
|
859
878
|
async getContacts(limit = 50) {
|
|
860
879
|
if (!this.client || !this.connected)
|
|
861
|
-
throw new Error(
|
|
880
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
862
881
|
const result = await this.client.invoke(new Api.contacts.GetContacts({ hash: bigInt(0) }));
|
|
863
882
|
if (!(result instanceof Api.contacts.Contacts))
|
|
864
883
|
return [];
|
|
@@ -878,7 +897,7 @@ export class TelegramService {
|
|
|
878
897
|
}
|
|
879
898
|
async getChatMembers(chatId, limit = 50) {
|
|
880
899
|
if (!this.client || !this.connected)
|
|
881
|
-
throw new Error(
|
|
900
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
882
901
|
const entity = await this.resolveChat(chatId);
|
|
883
902
|
if (entity instanceof Api.Channel) {
|
|
884
903
|
const result = await this.client.invoke(new Api.channels.GetParticipants({
|
|
@@ -923,7 +942,7 @@ export class TelegramService {
|
|
|
923
942
|
}
|
|
924
943
|
async getMyRole(chatId) {
|
|
925
944
|
if (!this.client || !this.connected)
|
|
926
|
-
throw new Error(
|
|
945
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
927
946
|
const entity = await this.resolveChat(chatId);
|
|
928
947
|
const me = await this.getMe();
|
|
929
948
|
if (entity instanceof Api.Channel) {
|
|
@@ -975,7 +994,7 @@ export class TelegramService {
|
|
|
975
994
|
}
|
|
976
995
|
async getProfile(userId) {
|
|
977
996
|
if (!this.client || !this.connected)
|
|
978
|
-
throw new Error(
|
|
997
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
979
998
|
const entity = await this.client.getEntity(userId);
|
|
980
999
|
if (!(entity instanceof Api.User))
|
|
981
1000
|
throw new Error("Entity is not a user");
|
|
@@ -1035,7 +1054,7 @@ export class TelegramService {
|
|
|
1035
1054
|
}
|
|
1036
1055
|
async downloadProfilePhoto(entityId, options) {
|
|
1037
1056
|
if (!this.client || !this.connected)
|
|
1038
|
-
throw new Error(
|
|
1057
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1039
1058
|
const entity = await this.client.getEntity(entityId);
|
|
1040
1059
|
const buffer = (await this.client.downloadProfilePhoto(entity, {
|
|
1041
1060
|
isBig: options?.isBig !== false,
|
|
@@ -1086,7 +1105,7 @@ export class TelegramService {
|
|
|
1086
1105
|
}
|
|
1087
1106
|
async sendReaction(chatId, messageId, emoji, addToExisting = false) {
|
|
1088
1107
|
if (!this.client || !this.connected)
|
|
1089
|
-
throw new Error(
|
|
1108
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1090
1109
|
const resolved = await this.resolvePeer(chatId);
|
|
1091
1110
|
const peer = await this.client.getInputEntity(resolved);
|
|
1092
1111
|
const reactionList = [];
|
|
@@ -1126,7 +1145,7 @@ export class TelegramService {
|
|
|
1126
1145
|
}
|
|
1127
1146
|
async getMessageReactions(chatId, messageId) {
|
|
1128
1147
|
if (!this.client || !this.connected)
|
|
1129
|
-
throw new Error(
|
|
1148
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1130
1149
|
const resolved = await this.resolvePeer(chatId);
|
|
1131
1150
|
const peer = await this.client.getInputEntity(resolved);
|
|
1132
1151
|
// First get the message to know which reactions exist
|
|
@@ -1181,7 +1200,7 @@ export class TelegramService {
|
|
|
1181
1200
|
}
|
|
1182
1201
|
async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
|
|
1183
1202
|
if (!this.client || !this.connected)
|
|
1184
|
-
throw new Error(
|
|
1203
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1185
1204
|
const resolved = await this.resolvePeer(chatId);
|
|
1186
1205
|
await this.client.sendMessage(resolved, {
|
|
1187
1206
|
message: text,
|
|
@@ -1192,7 +1211,7 @@ export class TelegramService {
|
|
|
1192
1211
|
}
|
|
1193
1212
|
async createPoll(chatId, question, answers, options) {
|
|
1194
1213
|
if (!this.client || !this.connected)
|
|
1195
|
-
throw new Error(
|
|
1214
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1196
1215
|
const peer = await this.client.getInputEntity(chatId);
|
|
1197
1216
|
const pollAnswers = answers.map((text, i) => new Api.PollAnswer({
|
|
1198
1217
|
text: new Api.TextWithEntities({ text, entities: [] }),
|
|
@@ -1228,7 +1247,7 @@ export class TelegramService {
|
|
|
1228
1247
|
}
|
|
1229
1248
|
async getForumTopics(chatId, limit = 100) {
|
|
1230
1249
|
if (!this.client || !this.connected)
|
|
1231
|
-
throw new Error(
|
|
1250
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1232
1251
|
const entity = await this.resolveChat(chatId);
|
|
1233
1252
|
if (!(entity instanceof Api.Channel))
|
|
1234
1253
|
throw new Error("Forum topics are only available in supergroups");
|
|
@@ -1257,7 +1276,7 @@ export class TelegramService {
|
|
|
1257
1276
|
}
|
|
1258
1277
|
async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
|
|
1259
1278
|
if (!this.client || !this.connected)
|
|
1260
|
-
throw new Error(
|
|
1279
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1261
1280
|
const peer = await this.client.getInputEntity(chatId);
|
|
1262
1281
|
const result = await this.client.invoke(new Api.messages.GetReplies({
|
|
1263
1282
|
peer,
|
|
@@ -1286,7 +1305,7 @@ export class TelegramService {
|
|
|
1286
1305
|
/** Check if a chat entity is a forum (has topics enabled) */
|
|
1287
1306
|
async isForum(chatId) {
|
|
1288
1307
|
if (!this.client || !this.connected)
|
|
1289
|
-
throw new Error(
|
|
1308
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1290
1309
|
try {
|
|
1291
1310
|
const entity = await this.resolveChat(chatId);
|
|
1292
1311
|
if (entity instanceof Api.Channel) {
|
|
@@ -1298,7 +1317,7 @@ export class TelegramService {
|
|
|
1298
1317
|
}
|
|
1299
1318
|
async joinChat(target) {
|
|
1300
1319
|
if (!this.client)
|
|
1301
|
-
throw new Error(
|
|
1320
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1302
1321
|
// Extract invite hash from various link formats
|
|
1303
1322
|
const inviteMatch = target.match(/(?:t\.me\/\+|t\.me\/joinchat\/|tg:\/\/join\?invite=)([a-zA-Z0-9_-]+)/);
|
|
1304
1323
|
if (inviteMatch) {
|
|
@@ -1329,7 +1348,7 @@ export class TelegramService {
|
|
|
1329
1348
|
}
|
|
1330
1349
|
async createGroup(options) {
|
|
1331
1350
|
if (!this.client)
|
|
1332
|
-
throw new Error(
|
|
1351
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1333
1352
|
const { title, users, supergroup = false, forum = false, description } = options;
|
|
1334
1353
|
if (supergroup || forum) {
|
|
1335
1354
|
// Create supergroup/channel via channels.CreateChannel
|
|
@@ -1403,7 +1422,7 @@ export class TelegramService {
|
|
|
1403
1422
|
}
|
|
1404
1423
|
async inviteToGroup(chatId, users) {
|
|
1405
1424
|
if (!this.client)
|
|
1406
|
-
throw new Error(
|
|
1425
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1407
1426
|
const entity = await this.resolveChat(chatId);
|
|
1408
1427
|
const invited = [];
|
|
1409
1428
|
const failed = [];
|
|
@@ -1431,7 +1450,7 @@ export class TelegramService {
|
|
|
1431
1450
|
}
|
|
1432
1451
|
async kickUser(chatId, userId) {
|
|
1433
1452
|
if (!this.client)
|
|
1434
|
-
throw new Error(
|
|
1453
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1435
1454
|
const entity = await this.resolveChat(chatId);
|
|
1436
1455
|
const user = await this.client.getEntity(userId);
|
|
1437
1456
|
if (!(user instanceof Api.User))
|
|
@@ -1456,7 +1475,7 @@ export class TelegramService {
|
|
|
1456
1475
|
}
|
|
1457
1476
|
async banUser(chatId, userId) {
|
|
1458
1477
|
if (!this.client)
|
|
1459
|
-
throw new Error(
|
|
1478
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1460
1479
|
const entity = await this.resolveChat(chatId);
|
|
1461
1480
|
const user = await this.client.getEntity(userId);
|
|
1462
1481
|
if (!(user instanceof Api.User))
|
|
@@ -1472,7 +1491,7 @@ export class TelegramService {
|
|
|
1472
1491
|
}
|
|
1473
1492
|
async unbanUser(chatId, userId) {
|
|
1474
1493
|
if (!this.client)
|
|
1475
|
-
throw new Error(
|
|
1494
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1476
1495
|
const entity = await this.resolveChat(chatId);
|
|
1477
1496
|
const user = await this.client.getEntity(userId);
|
|
1478
1497
|
if (!(user instanceof Api.User))
|
|
@@ -1488,7 +1507,7 @@ export class TelegramService {
|
|
|
1488
1507
|
}
|
|
1489
1508
|
async editGroup(chatId, options) {
|
|
1490
1509
|
if (!this.client)
|
|
1491
|
-
throw new Error(
|
|
1510
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1492
1511
|
const entity = await this.resolveChat(chatId);
|
|
1493
1512
|
if (options.title) {
|
|
1494
1513
|
if (entity instanceof Api.Channel) {
|
|
@@ -1518,7 +1537,7 @@ export class TelegramService {
|
|
|
1518
1537
|
}
|
|
1519
1538
|
async leaveGroup(chatId) {
|
|
1520
1539
|
if (!this.client)
|
|
1521
|
-
throw new Error(
|
|
1540
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1522
1541
|
const entity = await this.resolveChat(chatId);
|
|
1523
1542
|
if (entity instanceof Api.Channel) {
|
|
1524
1543
|
await this.client.invoke(new Api.channels.LeaveChannel({ channel: entity }));
|
|
@@ -1535,7 +1554,7 @@ export class TelegramService {
|
|
|
1535
1554
|
}
|
|
1536
1555
|
async setAdmin(chatId, userId, options) {
|
|
1537
1556
|
if (!this.client)
|
|
1538
|
-
throw new Error(
|
|
1557
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1539
1558
|
const entity = await this.resolveChat(chatId);
|
|
1540
1559
|
if (!(entity instanceof Api.Channel))
|
|
1541
1560
|
throw new Error("Set admin is only supported for supergroups and channels");
|
|
@@ -1561,7 +1580,7 @@ export class TelegramService {
|
|
|
1561
1580
|
}
|
|
1562
1581
|
async removeAdmin(chatId, userId) {
|
|
1563
1582
|
if (!this.client)
|
|
1564
|
-
throw new Error(
|
|
1583
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1565
1584
|
const entity = await this.resolveChat(chatId);
|
|
1566
1585
|
if (!(entity instanceof Api.Channel))
|
|
1567
1586
|
throw new Error("Remove admin is only supported for supergroups and channels");
|
package/dist/tools/auth.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { fail, ok, READ_ONLY, WRITE } from "./shared.js";
|
|
2
4
|
export function registerAuthTools(server, telegram) {
|
|
3
5
|
server.registerTool("telegram-status", { description: "Check Telegram connection status", annotations: READ_ONLY }, async () => {
|
|
@@ -18,11 +20,8 @@ export function registerAuthTools(server, telegram) {
|
|
|
18
20
|
annotations: WRITE,
|
|
19
21
|
}, async () => {
|
|
20
22
|
let qrDataUrl = "";
|
|
21
|
-
let qrRawUrl = "";
|
|
22
23
|
const loginPromise = telegram.startQrLogin((dataUrl) => {
|
|
23
24
|
qrDataUrl = dataUrl;
|
|
24
|
-
}, (url) => {
|
|
25
|
-
qrRawUrl = url;
|
|
26
25
|
});
|
|
27
26
|
// Wait for first QR to be generated
|
|
28
27
|
const startTime = Date.now();
|
|
@@ -41,20 +40,17 @@ export function registerAuthTools(server, telegram) {
|
|
|
41
40
|
console.error(`[mcp-telegram] Login failed: ${result.message}`);
|
|
42
41
|
}
|
|
43
42
|
});
|
|
44
|
-
//
|
|
43
|
+
// Save QR to file as fallback (no data sent to third-party services)
|
|
45
44
|
const base64 = qrDataUrl.replace(/^data:image\/png;base64,/, "");
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
: "";
|
|
45
|
+
const qrFilePath = join(telegram.sessionDir, "qr-login.png");
|
|
46
|
+
await writeFile(qrFilePath, Buffer.from(base64, "base64")).catch(() => { });
|
|
49
47
|
const instructions = [
|
|
50
48
|
"Scan this QR code in Telegram: **Settings → Devices → Link Desktop Device**.",
|
|
51
49
|
"",
|
|
52
|
-
|
|
50
|
+
`If the QR image is not visible, it's also saved to: ${qrFilePath}`,
|
|
53
51
|
"",
|
|
54
52
|
"After scanning, run **telegram-status** to verify the connection.",
|
|
55
|
-
]
|
|
56
|
-
.filter(Boolean)
|
|
57
|
-
.join("\n");
|
|
53
|
+
].join("\n");
|
|
58
54
|
return {
|
|
59
55
|
content: [
|
|
60
56
|
{
|
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",
|
|
@@ -25,7 +26,9 @@
|
|
|
25
26
|
"prepublishOnly": "npm run build",
|
|
26
27
|
"lint": "biome check src/",
|
|
27
28
|
"lint:fix": "biome check --fix src/",
|
|
28
|
-
"format": "biome format --write src/"
|
|
29
|
+
"format": "biome format --write src/",
|
|
30
|
+
"test": "tsx --test src/**/*.test.ts",
|
|
31
|
+
"test:watch": "tsx --test --watch src/**/*.test.ts"
|
|
29
32
|
},
|
|
30
33
|
"keywords": [
|
|
31
34
|
"mcp",
|