xzcgram 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/index.js +5 -0
- package/package.json +31 -0
- package/src/bot.js +156 -0
- package/src/client.js +91 -0
- package/src/context.js +295 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<div align='center'>
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/xzcgram)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/xzcgram)
|
|
6
|
+
|
|
7
|
+
[Donation site](https://www.zeppeli.my.id)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
#xzcgram
|
|
12
|
+
|
|
13
|
+
A tiny Telegraf-style router for [GramJS](https://github.com/gram-js/gramjs) (`telegram`).
|
|
14
|
+
|
|
15
|
+
Keeps GramJS's raw power (MTProto client, full TL API) but gives you a
|
|
16
|
+
familiar, minimal API on top of it:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
bot.command("start", async (ctx) => {
|
|
20
|
+
await ctx.reply("Hello!");
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
instead of manually wiring `client.addEventHandler` + `NewMessage` + parsing
|
|
25
|
+
the command yourself.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install xzcgram
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it — `telegram` (GramJS) ships as a direct dependency of this
|
|
34
|
+
package, so you don't need to install or import it separately.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const { clientStart } = require("xzcgram");
|
|
40
|
+
|
|
41
|
+
(async () => {
|
|
42
|
+
const { bot, sessionString } = await clientStart({
|
|
43
|
+
apiId: 123456,
|
|
44
|
+
apiHash: "your_api_hash",
|
|
45
|
+
session: process.env.SESSION || "", // saved session string, skips login
|
|
46
|
+
loginOptions: {
|
|
47
|
+
phoneNumber: async () => "+1234567890",
|
|
48
|
+
password: async () => "your2FApassword", // only if 2FA is enabled
|
|
49
|
+
phoneCode: async () => "12345",
|
|
50
|
+
onError: (err) => console.error(err),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log("Save this session string for next time:", sessionString);
|
|
55
|
+
|
|
56
|
+
bot.command("start", async (ctx) => {
|
|
57
|
+
await ctx.reply("Hello!");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
bot.hears(/hi|hello/i, async (ctx) => {
|
|
61
|
+
await ctx.reply("Hey there 👋");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
bot.on("message", async (ctx) => {
|
|
65
|
+
console.log("Unhandled message:", ctx.text);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await bot.launch();
|
|
69
|
+
})();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`clientStart` handles the GramJS client creation and login internally — no
|
|
73
|
+
need to `require("telegram")` anywhere in your code. See
|
|
74
|
+
[`example/basic.js`](./example/basic.js) for a full runnable example
|
|
75
|
+
(including interactive login prompts).
|
|
76
|
+
|
|
77
|
+
If you already manage your own `TelegramClient` elsewhere, you can still
|
|
78
|
+
use `Bot` directly instead of `clientStart` — see "Advanced usage" below.
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
### `clientStart(options)`
|
|
83
|
+
|
|
84
|
+
The recommended entry point. Creates a `TelegramClient`, logs it in, and
|
|
85
|
+
returns a ready-to-use `Bot`.
|
|
86
|
+
|
|
87
|
+
| Option | Required | Description |
|
|
88
|
+
| ------------------- | -------- | ---------------------------------------------------------- |
|
|
89
|
+
| `apiId` | yes | Your `api_id` from my.telegram.org |
|
|
90
|
+
| `apiHash` | yes | Your `api_hash` from my.telegram.org |
|
|
91
|
+
| `sessionType` | no | `"string"` (default) or `"store"` — see below |
|
|
92
|
+
| `session` | no | Saved session string, used when `sessionType` is `"string"` |
|
|
93
|
+
| `sessionName` | no | Session file name, used when `sessionType` is `"store"` (default `"xzcgram"`) |
|
|
94
|
+
| `clientOptions` | no | Extra options merged into the underlying `TelegramClient` |
|
|
95
|
+
| `loginOptions` | no | Callbacks forwarded to `client.start()` (phoneNumber, password, phoneCode, onError) |
|
|
96
|
+
|
|
97
|
+
Resolves to `{ bot, client, sessionString }`.
|
|
98
|
+
|
|
99
|
+
**Session strategies:**
|
|
100
|
+
|
|
101
|
+
- **`sessionType: "string"`** (default) — session is a portable string you
|
|
102
|
+
save yourself (env var, database, etc.) and pass back in as `session`
|
|
103
|
+
next run. `sessionString` in the return value holds it.
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const { bot, sessionString } = await clientStart({
|
|
107
|
+
apiId, apiHash,
|
|
108
|
+
sessionType: "string",
|
|
109
|
+
session: process.env.SESSION || "",
|
|
110
|
+
loginOptions: { /* ... */ },
|
|
111
|
+
});
|
|
112
|
+
console.log("Save this:", sessionString);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **`sessionType: "store"`** — session is written to a local file
|
|
116
|
+
automatically (GramJS's `StoreSession`), so there's no string to copy
|
|
117
|
+
around. Good for local scripts/servers where the file stays on disk.
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const { bot } = await clientStart({
|
|
121
|
+
apiId, apiHash,
|
|
122
|
+
sessionType: "store",
|
|
123
|
+
sessionName: "sessions", // creates my-bot-sessions.session
|
|
124
|
+
loginOptions: { /* ... */ },
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`sessionString` will be `null` in this mode since the session already
|
|
129
|
+
lives on disk.
|
|
130
|
+
|
|
131
|
+
### `new Bot(client)`
|
|
132
|
+
|
|
133
|
+
Lower-level constructor for when you already manage your own GramJS
|
|
134
|
+
`TelegramClient` (e.g. it's shared with other code). Doesn't connect or log
|
|
135
|
+
in for you — do that first with `client.start(...)`.
|
|
136
|
+
|
|
137
|
+
### `bot.command(name, handler)`
|
|
138
|
+
|
|
139
|
+
Registers a handler for a command. `name` can be with or without the
|
|
140
|
+
leading slash (`"start"` or `"/start"` both work). Matches `@botname`
|
|
141
|
+
suffixes too (`/start@mybot`).
|
|
142
|
+
|
|
143
|
+
### `bot.hears(pattern, handler)`
|
|
144
|
+
|
|
145
|
+
Registers a handler that fires when the message text matches `pattern`.
|
|
146
|
+
`pattern` can be a plain string (substring match) or a `RegExp`.
|
|
147
|
+
|
|
148
|
+
### `bot.on("message", handler)`
|
|
149
|
+
|
|
150
|
+
Fallback handler, fires for any message that didn't match a command or a
|
|
151
|
+
`hears` pattern.
|
|
152
|
+
|
|
153
|
+
### `bot.launch()`
|
|
154
|
+
|
|
155
|
+
Attaches the internal `NewMessage` event listener. Call once, after
|
|
156
|
+
registering your handlers.
|
|
157
|
+
|
|
158
|
+
### `Context` (passed to `command`/`hears`/`on` handlers)
|
|
159
|
+
|
|
160
|
+
| Property / method | Description |
|
|
161
|
+
| ------------------------------------------------ | ------------------------------------------------------------ |
|
|
162
|
+
| `ctx.client` | The underlying `TelegramClient` |
|
|
163
|
+
| `ctx.event` / `ctx.message` | Raw GramJS event / message object |
|
|
164
|
+
| `ctx.chatId` | Chat/peer id of the message |
|
|
165
|
+
| `ctx.text` | Full message text |
|
|
166
|
+
| `ctx.args` | Command arguments as an array (space-split) |
|
|
167
|
+
| **Text** | |
|
|
168
|
+
| `ctx.reply(text, opts?)` | Send a message to the same chat |
|
|
169
|
+
| `ctx.replyQuote(text, opts?)` | Reply directly to the triggering message |
|
|
170
|
+
| `ctx.editMessage(messageId, text, opts?)` | Edit a message you previously sent |
|
|
171
|
+
| **Media** | |
|
|
172
|
+
| `ctx.replyWithPhoto(file, opts?)` | Send a photo |
|
|
173
|
+
| `ctx.replyWithVideo(file, opts?)` | Send a video (streamable) |
|
|
174
|
+
| `ctx.replyWithVideoNote(file, opts?)` | Send a round video note |
|
|
175
|
+
| `ctx.replyWithAudio(file, opts?)` | Send an audio track |
|
|
176
|
+
| `ctx.replyWithVoice(file, opts?)` | Send a voice note |
|
|
177
|
+
| `ctx.replyWithDocument(file, opts?)` | Send any file as a document |
|
|
178
|
+
| `ctx.replyWithSticker(file, opts?)` | Send a sticker |
|
|
179
|
+
| `ctx.replyWithAnimation(file, opts?)` | Send a GIF |
|
|
180
|
+
| `ctx.replyWithMediaGroup(files, opts?)` | Send an album (array of files) |
|
|
181
|
+
| **Interactive** | |
|
|
182
|
+
| `ctx.replyWithButtons(text, rows, opts?)` | Send a message with inline buttons |
|
|
183
|
+
| `ctx.replyWithKeyboard(text, rows, opts?)` | Send a message with a plain reply keyboard |
|
|
184
|
+
| `ctx.replyWithPoll(question, answers, opts?)` | Send a poll |
|
|
185
|
+
| `ctx.replyWithLocation(lat, long, opts?)` | Send a location |
|
|
186
|
+
| `ctx.replyWithContact(phone, firstName, last?)` | Send a contact card |
|
|
187
|
+
| `ctx.replyWithDice(emoji?, opts?)` | Send an animated dice/emoji (🎲🎯🏀⚽🎰🎳) |
|
|
188
|
+
| **Chat management** | |
|
|
189
|
+
| `ctx.deleteMessage()` | Delete the triggering message (revoke for all) |
|
|
190
|
+
| `ctx.forwardMessage(toChatId)` | Forward the triggering message elsewhere |
|
|
191
|
+
| `ctx.pinMessage(opts?)` | Pin the triggering message |
|
|
192
|
+
| `ctx.unpinMessage()` | Unpin the triggering message |
|
|
193
|
+
| `ctx.sendChatAction(action?)` | Show "typing…" / "uploading photo…" etc |
|
|
194
|
+
| `ctx.getSender()` | Resolve the full sender entity |
|
|
195
|
+
| `ctx.getChat()` | Resolve the full chat entity |
|
|
196
|
+
|
|
197
|
+
Buttons (`replyWithButtons` / `replyWithKeyboard`) take rows of button
|
|
198
|
+
descriptors:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
await ctx.replyWithButtons("Pick one:", [
|
|
202
|
+
[{ text: "Yes", data: "confirm_yes" }, { text: "No", data: "confirm_no" }],
|
|
203
|
+
[{ text: "Telegram", url: "https://t.me/XazepysK" }],
|
|
204
|
+
]);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `bot.action(pattern, handler)`
|
|
208
|
+
|
|
209
|
+
Handles inline button presses (callback queries). `pattern` is matched
|
|
210
|
+
against the button's decoded `data` string — a plain string (exact match)
|
|
211
|
+
or a `RegExp`.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
bot.action(/^confirm_/, async (ctx) => {
|
|
215
|
+
await ctx.answer("Got it!"); // toast shown to the user
|
|
216
|
+
await ctx.editMessageText(`You picked: ${ctx.data}`);
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`CallbackContext` passed to `action` handlers:
|
|
221
|
+
|
|
222
|
+
| Property / method | Description |
|
|
223
|
+
| -------------------------------- | ------------------------------------------------ |
|
|
224
|
+
| `ctx.data` | Decoded callback data (string) |
|
|
225
|
+
| `ctx.dataRaw` | Raw callback data (Buffer) |
|
|
226
|
+
| `ctx.chatId` | Chat where the button was pressed |
|
|
227
|
+
| `ctx.answer(text?, opts?)` | Answer the callback query (toast/alert) |
|
|
228
|
+
| `ctx.editMessageText(text, opts?)` | Edit the message the button is attached to |
|
|
229
|
+
| `ctx.reply(text, opts?)` | Send a new message in that chat |
|
|
230
|
+
|
|
231
|
+
Anything not covered here is still reachable through `ctx.client` — GramJS's
|
|
232
|
+
full API (raw TL requests, `client.invoke(new Api...)`, etc.) is always
|
|
233
|
+
available.
|
|
234
|
+
|
|
235
|
+
## Advanced usage
|
|
236
|
+
|
|
237
|
+
If you already create/manage the `TelegramClient` yourself (custom
|
|
238
|
+
connection options, shared across multiple modules, etc.), skip `clientStart`
|
|
239
|
+
and use `Bot` directly:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
const { Bot } = require("xzcgram");
|
|
243
|
+
// require("telegram") yourself only if you need this lower-level control
|
|
244
|
+
const { TelegramClient } = require("telegram");
|
|
245
|
+
const { StringSession } = require("telegram/sessions");
|
|
246
|
+
|
|
247
|
+
const client = new TelegramClient(new StringSession(session), apiId, apiHash, {});
|
|
248
|
+
await client.start({ /* ... */ });
|
|
249
|
+
|
|
250
|
+
const bot = new Bot(client);
|
|
251
|
+
bot.command("start", async (ctx) => ctx.reply("Hello!"));
|
|
252
|
+
await bot.launch();
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Why
|
|
256
|
+
|
|
257
|
+
GramJS is a full MTProto client, not a bot framework — there's no built-in
|
|
258
|
+
concept of commands or routing, you handle raw events yourself. This package
|
|
259
|
+
adds just enough structure to make bot-style code readable, without hiding
|
|
260
|
+
GramJS underneath an opinionated abstraction.
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xzcgram",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A Telegraf-style command/hears router built on top of GramJS (telegram)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"homepage": "https://www.npmjs.com/package/xzcgram",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://www.npmjs.com/package/xzcgram"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"@zeppeliorg",
|
|
16
|
+
"gramjs",
|
|
17
|
+
"telegram",
|
|
18
|
+
"mtproto",
|
|
19
|
+
"telegraf",
|
|
20
|
+
"bot",
|
|
21
|
+
"router"
|
|
22
|
+
],
|
|
23
|
+
"author": "@zeppeliorg",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"telegram": "^2.19.10"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/bot.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const { NewMessage } = require("telegram/events");
|
|
2
|
+
const { NewCallbackQuery } = require("telegram/events/NewCallbackQuery");
|
|
3
|
+
const { Context } = require("./context");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CallbackContext wraps a GramJS NewCallbackQuery event, exposed to
|
|
7
|
+
* handlers registered via bot.action(...).
|
|
8
|
+
*/
|
|
9
|
+
class CallbackContext {
|
|
10
|
+
constructor(client, event) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.event = event;
|
|
13
|
+
this.chatId = event.chatId;
|
|
14
|
+
/** The raw callback data as a Buffer */
|
|
15
|
+
this.dataRaw = event.data;
|
|
16
|
+
/** The callback data decoded as a UTF-8 string */
|
|
17
|
+
this.data = event.data ? event.data.toString("utf-8") : "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Answer the callback query (e.g. show a small toast/alert to the user). */
|
|
21
|
+
answer(text = "", opts = {}) {
|
|
22
|
+
return this.event.answer({ message: text, ...opts });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Edit the message the inline button was attached to. */
|
|
26
|
+
editMessageText(text, opts = {}) {
|
|
27
|
+
return this.event.edit({ text, ...opts });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Send a new message in the same chat as the callback query. */
|
|
31
|
+
reply(text, opts = {}) {
|
|
32
|
+
return this.client.sendMessage(this.chatId, { message: text, ...opts });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bot provides a Telegraf-like API on top of a GramJS TelegramClient.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const bot = new Bot(client);
|
|
41
|
+
* bot.command("start", async (ctx) => ctx.reply("Hello!"));
|
|
42
|
+
* bot.action("confirm", async (ctx) => ctx.answer("Confirmed!"));
|
|
43
|
+
* await bot.launch();
|
|
44
|
+
*/
|
|
45
|
+
class Bot {
|
|
46
|
+
/**
|
|
47
|
+
* @param {import("telegram").TelegramClient} client an already-connected
|
|
48
|
+
* GramJS client (i.e. after `await client.start(...)`)
|
|
49
|
+
*/
|
|
50
|
+
constructor(client) {
|
|
51
|
+
this.client = client;
|
|
52
|
+
this.handlers = { command: {}, hears: [], on: {}, action: [] };
|
|
53
|
+
this._registered = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register a command handler.
|
|
58
|
+
* @param {string} cmd command name, with or without leading slash
|
|
59
|
+
* @param {(ctx: Context) => any} fn
|
|
60
|
+
*/
|
|
61
|
+
command(cmd, fn) {
|
|
62
|
+
this.handlers.command[cmd.replace(/^\//, "")] = fn;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Register a text matcher. Accepts a string (substring match)
|
|
68
|
+
* or a RegExp.
|
|
69
|
+
* @param {string|RegExp} pattern
|
|
70
|
+
* @param {(ctx: Context) => any} fn
|
|
71
|
+
*/
|
|
72
|
+
hears(pattern, fn) {
|
|
73
|
+
this.handlers.hears.push({ pattern, fn });
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register a handler for inline button presses (callback queries).
|
|
79
|
+
* @param {string|RegExp} pattern matched against the button's decoded data
|
|
80
|
+
* @param {(ctx: CallbackContext) => any} fn
|
|
81
|
+
*/
|
|
82
|
+
action(pattern, fn) {
|
|
83
|
+
this.handlers.action.push({ pattern, fn });
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register a fallback handler.
|
|
89
|
+
* Currently supported event: "message" (fires for any message that
|
|
90
|
+
* didn't match a command or hears pattern).
|
|
91
|
+
* @param {"message"} event
|
|
92
|
+
* @param {(ctx: Context) => any} fn
|
|
93
|
+
*/
|
|
94
|
+
on(event, fn) {
|
|
95
|
+
this.handlers.on[event] = fn;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Attach the internal event listeners to the client. Safe to call once;
|
|
101
|
+
* subsequent calls are no-ops.
|
|
102
|
+
*/
|
|
103
|
+
async launch() {
|
|
104
|
+
if (this._registered) return;
|
|
105
|
+
this._registered = true;
|
|
106
|
+
|
|
107
|
+
this.client.addEventHandler(async (event) => {
|
|
108
|
+
try {
|
|
109
|
+
const msg = event.message;
|
|
110
|
+
if (!msg || !msg.message) return;
|
|
111
|
+
|
|
112
|
+
const ctx = new Context(this.client, event);
|
|
113
|
+
const text = ctx.text;
|
|
114
|
+
|
|
115
|
+
if (text.startsWith("/")) {
|
|
116
|
+
const cmd = text.slice(1).split(" ")[0].split("@")[0];
|
|
117
|
+
if (this.handlers.command[cmd]) {
|
|
118
|
+
return await this.handlers.command[cmd](ctx);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const h of this.handlers.hears) {
|
|
123
|
+
const matched =
|
|
124
|
+
h.pattern instanceof RegExp
|
|
125
|
+
? h.pattern.test(text)
|
|
126
|
+
: text.includes(h.pattern);
|
|
127
|
+
if (matched) return await h.fn(ctx);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.handlers.on.message) {
|
|
131
|
+
return await this.handlers.on.message(ctx);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error("[xzcgram] message handler error:", err);
|
|
135
|
+
}
|
|
136
|
+
}, new NewMessage({}));
|
|
137
|
+
|
|
138
|
+
this.client.addEventHandler(async (event) => {
|
|
139
|
+
try {
|
|
140
|
+
const ctx = new CallbackContext(this.client, event);
|
|
141
|
+
|
|
142
|
+
for (const h of this.handlers.action) {
|
|
143
|
+
const matched =
|
|
144
|
+
h.pattern instanceof RegExp
|
|
145
|
+
? h.pattern.test(ctx.data)
|
|
146
|
+
: ctx.data === h.pattern;
|
|
147
|
+
if (matched) return await h.fn(ctx);
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("[xzcgram] action handler error:", err);
|
|
151
|
+
}
|
|
152
|
+
}, new NewCallbackQuery({}));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { Bot, Context, CallbackContext };
|
package/src/client.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const { TelegramClient } = require("telegram");
|
|
2
|
+
const { StringSession, StoreSession } = require("telegram/sessions");
|
|
3
|
+
const { Bot } = require("./bot");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a logged-in TelegramClient and wraps it in a Bot instance.
|
|
7
|
+
* Handles the "telegram" (GramJS) imports internally, so the consumer
|
|
8
|
+
* of this package never has to require("telegram") themselves.
|
|
9
|
+
*
|
|
10
|
+
* Supports two session strategies:
|
|
11
|
+
* - "string" (default): session is kept as a portable string you save
|
|
12
|
+
* yourself (e.g. in an env var). Pass it back in via `session` next run.
|
|
13
|
+
* - "store": session is persisted to a local file on disk via GramJS's
|
|
14
|
+
* StoreSession, so you don't have to manage the string manually.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {number} options.apiId - your api_id from my.telegram.org
|
|
18
|
+
* @param {string} options.apiHash - your api_hash from my.telegram.org
|
|
19
|
+
* @param {"string"|"store"} [options.sessionType="string"] - session strategy to use
|
|
20
|
+
* @param {string} [options.session] - saved session string (only used when sessionType is "string")
|
|
21
|
+
* @param {string} [options.sessionName="gramjs-router"] - session file name (only used when sessionType is "store")
|
|
22
|
+
* @param {object} [options.clientOptions] - extra options for TelegramClient
|
|
23
|
+
* @param {object} [options.loginOptions] - callbacks for client.start()
|
|
24
|
+
* (phoneNumber, password, phoneCode, onError, etc.)
|
|
25
|
+
*
|
|
26
|
+
* @returns {Promise<{ bot: Bot, client: import("telegram").TelegramClient, sessionString: string|null }>}
|
|
27
|
+
* `sessionString` is null when sessionType is "store", since the session
|
|
28
|
+
* already lives on disk and there's nothing to save manually.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // StringSession (portable, you manage the string yourself)
|
|
32
|
+
* const { bot, sessionString } = await clientStart({
|
|
33
|
+
* apiId, apiHash,
|
|
34
|
+
* sessionType: "string",
|
|
35
|
+
* session: process.env.SESSION || "",
|
|
36
|
+
* loginOptions: { ... },
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // StoreSession (persisted to a local file automatically)
|
|
41
|
+
* const { bot } = await clientStart({
|
|
42
|
+
* apiId, apiHash,
|
|
43
|
+
* sessionType: "store",
|
|
44
|
+
* sessionName: "my-bot-session",
|
|
45
|
+
* loginOptions: { ... },
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
48
|
+
async function clientStart({
|
|
49
|
+
apiId,
|
|
50
|
+
apiHash,
|
|
51
|
+
sessionType = "string",
|
|
52
|
+
session = "",
|
|
53
|
+
sessionName = "sessions",
|
|
54
|
+
clientOptions = {},
|
|
55
|
+
loginOptions = {},
|
|
56
|
+
} = {}) {
|
|
57
|
+
if (!apiId || !apiHash) {
|
|
58
|
+
throw new Error("clientStart requires both apiId and apiHash");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let sessionInstance;
|
|
62
|
+
if (sessionType === "store") {
|
|
63
|
+
// Persists session data to a local file (e.g. "./sessions.session")
|
|
64
|
+
sessionInstance = new StoreSession(sessionName);
|
|
65
|
+
} else if (sessionType === "string") {
|
|
66
|
+
// Keeps session as a string the caller saves/restores manually
|
|
67
|
+
sessionInstance = new StringSession(session);
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid sessionType "${sessionType}". Use "string" or "store".`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const client = new TelegramClient(sessionInstance, apiId, apiHash, {
|
|
75
|
+
connectionRetries: 5,
|
|
76
|
+
...clientOptions,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await client.start(loginOptions);
|
|
80
|
+
|
|
81
|
+
const bot = new Bot(client);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
bot,
|
|
85
|
+
client,
|
|
86
|
+
// StoreSession writes to disk on its own; there's no string to hand back.
|
|
87
|
+
sessionString: sessionType === "string" ? client.session.save() : null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = clientStart;
|
package/src/context.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
const { Api } = require("telegram");
|
|
2
|
+
const { Button } = require("telegram/tl/custom/button");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a simple button layout into GramJS Button instances.
|
|
6
|
+
* Accepts an array of rows, each row an array of button descriptors:
|
|
7
|
+
* { text, data } -> inline callback button
|
|
8
|
+
* { text, url } -> inline URL button
|
|
9
|
+
* { text } -> plain reply keyboard button (not inline)
|
|
10
|
+
*
|
|
11
|
+
* @param {Array<Array<{text: string, data?: string, url?: string}>>} rows
|
|
12
|
+
* @param {boolean} [inline=true]
|
|
13
|
+
*/
|
|
14
|
+
function buildButtons(rows, inline = true) {
|
|
15
|
+
return rows.map((row) =>
|
|
16
|
+
row.map((btn) => {
|
|
17
|
+
if (btn.url) return Button.url(btn.text, btn.url);
|
|
18
|
+
if (inline) return Button.inline(btn.text, Buffer.from(btn.data || btn.text));
|
|
19
|
+
return Button.text(btn.text);
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Context wraps a GramJS NewMessage event and exposes
|
|
26
|
+
* Telegraf-style helpers (ctx.reply, ctx.replyWithVideo, etc).
|
|
27
|
+
*/
|
|
28
|
+
class Context {
|
|
29
|
+
constructor(client, event) {
|
|
30
|
+
/** @type {import("telegram").TelegramClient} */
|
|
31
|
+
this.client = client;
|
|
32
|
+
/** Raw GramJS event object */
|
|
33
|
+
this.event = event;
|
|
34
|
+
/** Raw GramJS message object */
|
|
35
|
+
this.message = event.message;
|
|
36
|
+
/** Chat/peer id where the message was sent */
|
|
37
|
+
this.chatId = this.message.chatId;
|
|
38
|
+
/** Full text of the incoming message */
|
|
39
|
+
this.text = this.message.message || "";
|
|
40
|
+
/** Command arguments, e.g. "/start foo bar" -> ["foo", "bar"] */
|
|
41
|
+
this.args = this.text.split(" ").slice(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------
|
|
45
|
+
// Text
|
|
46
|
+
// ---------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** Reply in the same chat the message came from. */
|
|
49
|
+
reply(text, opts = {}) {
|
|
50
|
+
return this.client.sendMessage(this.chatId, { message: text, ...opts });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reply directly to the triggering message (quote-style reply). */
|
|
54
|
+
replyQuote(text, opts = {}) {
|
|
55
|
+
return this.client.sendMessage(this.chatId, {
|
|
56
|
+
message: text,
|
|
57
|
+
replyTo: this.message.id,
|
|
58
|
+
...opts,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Edit a message previously sent in this chat (must be your own message). */
|
|
63
|
+
editMessage(messageId, text, opts = {}) {
|
|
64
|
+
return this.client.editMessage(this.chatId, {
|
|
65
|
+
message: messageId,
|
|
66
|
+
text,
|
|
67
|
+
...opts,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------
|
|
72
|
+
// Media
|
|
73
|
+
// ---------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/** Send a photo. `file` can be a path, Buffer, URL, or existing file id. */
|
|
76
|
+
replyWithPhoto(file, opts = {}) {
|
|
77
|
+
return this.client.sendFile(this.chatId, { file, ...opts });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Send a video. Pass `opts.supportsStreaming = true` for streamable playback. */
|
|
81
|
+
replyWithVideo(file, opts = {}) {
|
|
82
|
+
return this.client.sendFile(this.chatId, {
|
|
83
|
+
file,
|
|
84
|
+
supportsStreaming: true,
|
|
85
|
+
...opts,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Send a round "video note" (the circular video bubble). */
|
|
90
|
+
replyWithVideoNote(file, opts = {}) {
|
|
91
|
+
return this.client.sendFile(this.chatId, { file, videoNote: true, ...opts });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Send an audio file (music, shown with player + duration/title). */
|
|
95
|
+
replyWithAudio(file, opts = {}) {
|
|
96
|
+
return this.client.sendFile(this.chatId, { file, ...opts });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Send a voice note (the waveform bubble). */
|
|
100
|
+
replyWithVoice(file, opts = {}) {
|
|
101
|
+
return this.client.sendFile(this.chatId, { file, voiceNote: true, ...opts });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Send any file as a generic document. */
|
|
105
|
+
replyWithDocument(file, opts = {}) {
|
|
106
|
+
return this.client.sendFile(this.chatId, {
|
|
107
|
+
file,
|
|
108
|
+
forceDocument: true,
|
|
109
|
+
...opts,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Send a sticker (.webp/.tgs file or existing file reference). */
|
|
114
|
+
replyWithSticker(file, opts = {}) {
|
|
115
|
+
return this.client.sendFile(this.chatId, { file, ...opts });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Send an animated GIF. */
|
|
119
|
+
replyWithAnimation(file, opts = {}) {
|
|
120
|
+
return this.client.sendFile(this.chatId, {
|
|
121
|
+
file,
|
|
122
|
+
attributes: [new Api.DocumentAttributeAnimated()],
|
|
123
|
+
...opts,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Send multiple files as an album/media group. `files` is an array. */
|
|
128
|
+
replyWithMediaGroup(files, opts = {}) {
|
|
129
|
+
return this.client.sendFile(this.chatId, { file: files, ...opts });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------
|
|
133
|
+
// Interactive / structured content
|
|
134
|
+
// ---------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Send a message with inline buttons.
|
|
138
|
+
* @param {string} text
|
|
139
|
+
* @param {Array<Array<{text: string, data?: string, url?: string}>>} rows
|
|
140
|
+
* @param {object} [opts]
|
|
141
|
+
*/
|
|
142
|
+
replyWithButtons(text, rows, opts = {}) {
|
|
143
|
+
return this.client.sendMessage(this.chatId, {
|
|
144
|
+
message: text,
|
|
145
|
+
buttons: buildButtons(rows, true),
|
|
146
|
+
...opts,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Send a plain (non-inline) reply keyboard.
|
|
152
|
+
* @param {string} text
|
|
153
|
+
* @param {Array<Array<{text: string}>>} rows
|
|
154
|
+
* @param {object} [opts]
|
|
155
|
+
*/
|
|
156
|
+
replyWithKeyboard(text, rows, opts = {}) {
|
|
157
|
+
return this.client.sendMessage(this.chatId, {
|
|
158
|
+
message: text,
|
|
159
|
+
buttons: buildButtons(rows, false),
|
|
160
|
+
...opts,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Send a poll.
|
|
166
|
+
* @param {string} question
|
|
167
|
+
* @param {string[]} answers
|
|
168
|
+
* @param {object} [opts] e.g. { multipleChoice: true, quiz: false }
|
|
169
|
+
*/
|
|
170
|
+
replyWithPoll(question, answers, opts = {}) {
|
|
171
|
+
return this.client.invoke(
|
|
172
|
+
new Api.messages.SendMedia({
|
|
173
|
+
peer: this.chatId,
|
|
174
|
+
media: new Api.InputMediaPoll({
|
|
175
|
+
poll: new Api.Poll({
|
|
176
|
+
id: BigInt(Date.now()),
|
|
177
|
+
question,
|
|
178
|
+
answers: answers.map(
|
|
179
|
+
(text, i) =>
|
|
180
|
+
new Api.PollAnswer({ text, option: Buffer.from([i]) })
|
|
181
|
+
),
|
|
182
|
+
multipleChoice: !!opts.multipleChoice,
|
|
183
|
+
}),
|
|
184
|
+
}),
|
|
185
|
+
message: "",
|
|
186
|
+
randomId: BigInt(Date.now()),
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Send a geographic location. */
|
|
192
|
+
replyWithLocation(latitude, longitude, opts = {}) {
|
|
193
|
+
return this.client.invoke(
|
|
194
|
+
new Api.messages.SendMedia({
|
|
195
|
+
peer: this.chatId,
|
|
196
|
+
media: new Api.InputMediaGeoPoint({
|
|
197
|
+
geoPoint: new Api.InputGeoPoint({ lat: latitude, long: longitude }),
|
|
198
|
+
}),
|
|
199
|
+
message: "",
|
|
200
|
+
randomId: BigInt(Date.now()),
|
|
201
|
+
...opts,
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Send a contact card. */
|
|
207
|
+
replyWithContact(phoneNumber, firstName, lastName = "", opts = {}) {
|
|
208
|
+
return this.client.invoke(
|
|
209
|
+
new Api.messages.SendMedia({
|
|
210
|
+
peer: this.chatId,
|
|
211
|
+
media: new Api.InputMediaContact({
|
|
212
|
+
phoneNumber,
|
|
213
|
+
firstName,
|
|
214
|
+
lastName,
|
|
215
|
+
vcard: "",
|
|
216
|
+
}),
|
|
217
|
+
message: "",
|
|
218
|
+
randomId: BigInt(Date.now()),
|
|
219
|
+
...opts,
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Send an animated dice/emoji reaction (🎲 🎯 🏀 ⚽ 🎰 🎳). */
|
|
225
|
+
replyWithDice(emoji = "🎰", opts = {}) {
|
|
226
|
+
return this.client.invoke(
|
|
227
|
+
new Api.messages.SendMedia({
|
|
228
|
+
peer: this.chatId,
|
|
229
|
+
media: new Api.InputMediaDice({ emoticon: emoji }),
|
|
230
|
+
message: "",
|
|
231
|
+
randomId: BigInt(Date.now()),
|
|
232
|
+
...opts,
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------
|
|
238
|
+
// Chat / message management
|
|
239
|
+
// ---------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
/** Delete the triggering message (revokes for everyone by default). */
|
|
242
|
+
deleteMessage() {
|
|
243
|
+
return this.client.deleteMessages(this.chatId, [this.message.id], {
|
|
244
|
+
revoke: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Forward the triggering message to another chat. */
|
|
249
|
+
forwardMessage(toChatId) {
|
|
250
|
+
return this.client.forwardMessages(toChatId, {
|
|
251
|
+
messages: [this.message.id],
|
|
252
|
+
fromPeer: this.chatId,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Pin the triggering message in the current chat. */
|
|
257
|
+
pinMessage(opts = {}) {
|
|
258
|
+
return this.client.pinMessage(this.chatId, this.message.id, opts);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Unpin the triggering message in the current chat. */
|
|
262
|
+
unpinMessage() {
|
|
263
|
+
return this.client.unpinMessage(this.chatId, this.message.id);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Show the "typing..." / "sending photo..." indicator. */
|
|
267
|
+
sendChatAction(action = "typing") {
|
|
268
|
+
const actions = {
|
|
269
|
+
typing: new Api.SendMessageTypingAction(),
|
|
270
|
+
photo: new Api.SendMessageUploadPhotoAction({ progress: 0 }),
|
|
271
|
+
video: new Api.SendMessageUploadVideoAction({ progress: 0 }),
|
|
272
|
+
audio: new Api.SendMessageUploadAudioAction({ progress: 0 }),
|
|
273
|
+
document: new Api.SendMessageUploadDocumentAction({ progress: 0 }),
|
|
274
|
+
cancel: new Api.SendMessageCancelAction(),
|
|
275
|
+
};
|
|
276
|
+
return this.client.invoke(
|
|
277
|
+
new Api.messages.SetTyping({
|
|
278
|
+
peer: this.chatId,
|
|
279
|
+
action: actions[action] || actions.typing,
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Get the full entity (User/Chat/Channel) of whoever sent the message. */
|
|
285
|
+
getSender() {
|
|
286
|
+
return this.message.getSender();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Get the full entity (User/Chat/Channel) of the current chat. */
|
|
290
|
+
getChat() {
|
|
291
|
+
return this.message.getChat();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = { Context, buildButtons };
|