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 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
+ [![npm version](https://img.shields.io/npm/v/xzcgram.svg)](https://www.npmjs.com/package/xzcgram)
4
+ [![License](https://img.shields.io/badge/license-GPL%203-blue.svg)](LICENSE)
5
+ [![Downloads](https://img.shields.io/npm/dm/xzcgram.svg)](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
@@ -0,0 +1,5 @@
1
+ const { Button } = require("telegram/tl/custom/button");
2
+ const { Bot, Context, CallbackContext } = require("./src/bot");
3
+ const clientStart = require("./src/client");
4
+
5
+ module.exports = { Bot, Context, CallbackContext, clientStart, Button };
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 };