xzcgram 0.0.2 → 0.0.3
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.
Potentially problematic release.
This version of xzcgram might be problematic. Click here for more details.
- package/README.md +193 -7
- package/index.js +6 -2
- package/package.json +4 -2
- package/src/bot.js +156 -4
- package/src/client.js +32 -8
- package/src/context.js +759 -46
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
#xzcgram
|
|
11
|
+
# xzcgram
|
|
12
12
|
|
|
13
13
|
A tiny Telegraf-style router for [GramJS](https://github.com/gram-js/gramjs) (`telegram`).
|
|
14
14
|
|
|
@@ -88,6 +88,7 @@ returns a ready-to-use `Bot`.
|
|
|
88
88
|
| ------------------- | -------- | ---------------------------------------------------------- |
|
|
89
89
|
| `apiId` | yes | Your `api_id` from my.telegram.org |
|
|
90
90
|
| `apiHash` | yes | Your `api_hash` from my.telegram.org |
|
|
91
|
+
| `botToken` | no | A bot token from @BotFather — logs in as that **bot** instead of a user account (skips `loginOptions` entirely) |
|
|
91
92
|
| `sessionType` | no | `"string"` (default) or `"store"` — see below |
|
|
92
93
|
| `session` | no | Saved session string, used when `sessionType` is `"string"` |
|
|
93
94
|
| `sessionName` | no | Session file name, used when `sessionType` is `"store"` (default `"xzcgram"`) |
|
|
@@ -128,6 +129,40 @@ Resolves to `{ bot, client, sessionString }`.
|
|
|
128
129
|
`sessionString` will be `null` in this mode since the session already
|
|
129
130
|
lives on disk.
|
|
130
131
|
|
|
132
|
+
**Logging in as a bot instead of a user:**
|
|
133
|
+
|
|
134
|
+
Pass `botToken` (from [@BotFather](https://t.me/BotFather)) and skip
|
|
135
|
+
`loginOptions` entirely — no phone number, code, or password needed:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
const { bot } = await clientStart({
|
|
139
|
+
apiId,
|
|
140
|
+
apiHash,
|
|
141
|
+
botToken: "123456:ABC-your-bot-token-from-BotFather",
|
|
142
|
+
sessionType: "store",
|
|
143
|
+
sessionName: "my-bot",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
bot.command("start", async (ctx) => {
|
|
147
|
+
await ctx.reply("Hello! I'm running as a bot account.");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await bot.launch();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Everything else (`bot.command`, `bot.hears`, `bot.action`, `ctx.reply*`,
|
|
154
|
+
etc.) works the same either way. A couple of things differ because
|
|
155
|
+
Telegram treats bot accounts differently from user accounts:
|
|
156
|
+
- Bots can only see messages sent directly to them or in chats/groups
|
|
157
|
+
they've been added to — they can't read arbitrary chats like a user
|
|
158
|
+
session can.
|
|
159
|
+
- Some `ctx.*` methods that act on "your own account" (`updateProfile`,
|
|
160
|
+
`updateUsername`, `setOnlineStatus`, `getPrivacySettings`, `blockUser`,
|
|
161
|
+
etc.) either behave differently or aren't meaningful for bots — those
|
|
162
|
+
are really aimed at user-account sessions.
|
|
163
|
+
- Admin-type actions (`banUser`, `promoteUser`, etc.) still work, but the
|
|
164
|
+
bot itself needs the relevant admin rights in that chat first.
|
|
165
|
+
|
|
131
166
|
### `new Bot(client)`
|
|
132
167
|
|
|
133
168
|
Lower-level constructor for when you already manage your own GramJS
|
|
@@ -164,6 +199,20 @@ registering your handlers.
|
|
|
164
199
|
| `ctx.chatId` | Chat/peer id of the message |
|
|
165
200
|
| `ctx.text` | Full message text |
|
|
166
201
|
| `ctx.args` | Command arguments as an array (space-split) |
|
|
202
|
+
| **Metadata** | |
|
|
203
|
+
| `ctx.senderId` | Id of whoever sent the message |
|
|
204
|
+
| `ctx.date` / `ctx.editDate` | Unix timestamp sent / last edited |
|
|
205
|
+
| `ctx.viaBotId` | Id of the inline bot used to send this, if any |
|
|
206
|
+
| `ctx.viaBusinessBotId` | Id of the Business-connected bot used to send this, if any |
|
|
207
|
+
| `ctx.isPrivate` / `ctx.isGroup` / `ctx.isChannel`| What kind of chat this is |
|
|
208
|
+
| `ctx.replyToMsgId` / `ctx.isReply` | Id of the message being replied to, if any |
|
|
209
|
+
| `ctx.replyMessage` | **Promise** resolving to the replied-to message(s) (`[]` if not a reply) — `await` it |
|
|
210
|
+
| `ctx.fwdFrom` / `ctx.isForwarded` | Forward header info, if the message was forwarded |
|
|
211
|
+
| `ctx.entities` | Message entities (bold, links, mentions, etc.) |
|
|
212
|
+
| `ctx.media` / `ctx.hasMedia` / `ctx.mediaType` | Raw attached media, whether it exists, and its type |
|
|
213
|
+
| `ctx.silent` / `ctx.out` / `ctx.pinned` | Sent silently / sent by us / currently pinned |
|
|
214
|
+
| `ctx.groupedId` | Album id, if this message is part of a media group |
|
|
215
|
+
| `ctx.peer()` | Resolves the correct input peer for the current chat (used internally, but callable directly) |
|
|
167
216
|
| **Text** | |
|
|
168
217
|
| `ctx.reply(text, opts?)` | Send a message to the same chat |
|
|
169
218
|
| `ctx.replyQuote(text, opts?)` | Reply directly to the triggering message |
|
|
@@ -181,18 +230,91 @@ registering your handlers.
|
|
|
181
230
|
| **Interactive** | |
|
|
182
231
|
| `ctx.replyWithButtons(text, rows, opts?)` | Send a message with inline buttons |
|
|
183
232
|
| `ctx.replyWithKeyboard(text, rows, opts?)` | Send a message with a plain reply keyboard |
|
|
184
|
-
| `ctx.replyWithPoll(question, answers, opts?)` | Send a poll
|
|
233
|
+
| `ctx.replyWithPoll(question, answers, opts?)` | Send a poll (`opts`: `multipleChoice`, `quiz`, `publicVoters`) |
|
|
185
234
|
| `ctx.replyWithLocation(lat, long, opts?)` | Send a location |
|
|
186
235
|
| `ctx.replyWithContact(phone, firstName, last?)` | Send a contact card |
|
|
187
|
-
| `ctx.replyWithDice(emoji?, opts?)` | Send an animated dice/emoji (
|
|
236
|
+
| `ctx.replyWithDice(emoji?, opts?)` | Send an animated dice/emoji (default 🎰; also 🎲🎯🏀⚽🎳) |
|
|
188
237
|
| **Chat management** | |
|
|
189
|
-
| `ctx.deleteMessage()`
|
|
190
|
-
| `ctx.forwardMessage(toChatId)`
|
|
191
|
-
| `ctx.pinMessage(opts?)`
|
|
192
|
-
| `ctx.unpinMessage()`
|
|
238
|
+
| `ctx.deleteMessage(id?)` | Delete a message (defaults to the triggering one, revokes for all) |
|
|
239
|
+
| `ctx.forwardMessage(toChatId, id?)` | Forward a message (defaults to the triggering one) elsewhere |
|
|
240
|
+
| `ctx.pinMessage(id?, opts?)` | Pin a message (defaults to the triggering one) |
|
|
241
|
+
| `ctx.unpinMessage(id?)` | Unpin a message (defaults to the triggering one) |
|
|
193
242
|
| `ctx.sendChatAction(action?)` | Show "typing…" / "uploading photo…" etc |
|
|
194
243
|
| `ctx.getSender()` | Resolve the full sender entity |
|
|
195
244
|
| `ctx.getChat()` | Resolve the full chat entity |
|
|
245
|
+
| `ctx.replyViaBot(bot, query?, opts?)` | Send a message "via @bot" using an inline query result |
|
|
246
|
+
| **More media / interaction** | |
|
|
247
|
+
| `ctx.replyWithVenue(lat, long, title, address, opts?)` | Send a venue (pinned location + name/address) |
|
|
248
|
+
| `ctx.replyWithLiveLocation(lat, long, period?, opts?)` | Send a live-updating location (period in seconds, 60–86400) |
|
|
249
|
+
| `ctx.react(id?, emoji?, opts?)` | React to a message (defaults to the triggering one); empty/null emoji removes it |
|
|
250
|
+
| `ctx.copyMessage(toChatId, opts?)` | Re-send the triggering message's content elsewhere, no "Forwarded from" header |
|
|
251
|
+
| `ctx.markAsRead(peer?)` | Mark a chat as read (defaults to the current chat) |
|
|
252
|
+
| `ctx.downloadMedia(opts?)` | Download the triggering message's attached media (Buffer, or see `saveMediaMessage`) |
|
|
253
|
+
| `ctx.editMessageMedia(messageId, file, opts?)` | Swap the media on a message you previously sent |
|
|
254
|
+
| **Voice / text / poll** | |
|
|
255
|
+
| `ctx.transcribeVoice(id?)` | Transcribe a voice note (defaults to the triggering message) |
|
|
256
|
+
| `ctx.translateText(text, toLang)` | Translate arbitrary text (pass the text explicitly, e.g. `ctx.text`) |
|
|
257
|
+
| `ctx.votePoll(id, options)` | Vote on a poll message — **`id` is required as the 1st arg**, pass `undefined` to default to the triggering message (see note below) |
|
|
258
|
+
| `ctx.retractVote(id?)` | Retract your vote on that poll |
|
|
259
|
+
| **Contacts / history** | |
|
|
260
|
+
| `ctx.blockUser(userId?)` / `ctx.unblockUser(userId?)` | Block/unblock a user (defaults to the sender of the triggering message) |
|
|
261
|
+
| `ctx.getHistory(peer?, limit?, opts?)` | Fetch recent messages (defaults to the current chat) |
|
|
262
|
+
| `ctx.searchMessages(query, opts?)` | Search messages in the current chat |
|
|
263
|
+
| `ctx.unpinAllMessages(peer?)` | Unpin every pinned message (defaults to the current chat) |
|
|
264
|
+
| `ctx.getParticipants(opts?)` | List members of the current group/channel |
|
|
265
|
+
| `ctx.getEntityByUsername(username)` | Get metadata for a user/chat/channel by @username |
|
|
266
|
+
| **Channel/group management** *(supergroup/channel unless noted)* | |
|
|
267
|
+
| `ctx.banUser(userId, opts?)` | Ban a user (can't rejoin unless unbanned) |
|
|
268
|
+
| `ctx.unbanUser(userId)` | Clear all restrictions on a user |
|
|
269
|
+
| `ctx.kickUser(userId)` | Remove a user, but they CAN rejoin (works in basic groups too) |
|
|
270
|
+
| `ctx.promoteUser(userId, rights?, rank?)` | Make a user admin |
|
|
271
|
+
| `ctx.demoteUser(userId)` | Strip a user's admin rights |
|
|
272
|
+
| `ctx.inviteToChannel(userIds)` | Invite one or more users to the current channel/supergroup |
|
|
273
|
+
| `ctx.setChatTitle(id, title)` | Rename a chat — **pass `null`/`undefined` as `id` to target the current chat** (see note below) |
|
|
274
|
+
| `ctx.setChatPhoto(id, file)` | Change a chat's photo — same `id` rule as `setChatTitle` |
|
|
275
|
+
| `ctx.joinChannel(id)` | Join a channel/group by @username, id, or entity |
|
|
276
|
+
| `ctx.joinByInvite(hash)` | Join a private chat via invite hash (the part after `t.me/+`) |
|
|
277
|
+
| `ctx.leaveChannel(id?)` | Leave a channel/group (defaults to the current chat) |
|
|
278
|
+
| **Stickers & GIFs** | |
|
|
279
|
+
| `ctx.searchStickers(emoji)` | Find stickers matching an emoji |
|
|
280
|
+
| `ctx.getStickerSet(shortName)` | Get a sticker set's full info + documents |
|
|
281
|
+
| `ctx.searchGifs(query, offset?)` | Search Telegram's global GIF results |
|
|
282
|
+
| `ctx.saveGif(document, unsave?)` | Save/unsave a GIF to "Saved GIFs" |
|
|
283
|
+
| **Privacy & account settings** | |
|
|
284
|
+
| `ctx.getPrivacySettings(key?)` | Get your privacy rules for a key (phoneNumber, lastSeen, etc.) |
|
|
285
|
+
| `ctx.updateProfile(opts?)` | Update your first/last name and bio |
|
|
286
|
+
| `ctx.updateUsername(username)` | Change your @username |
|
|
287
|
+
| `ctx.setOnlineStatus(online?)` | Manually set your online/offline status |
|
|
288
|
+
| **Advanced file uploads** | |
|
|
289
|
+
| `ctx.replyWithSpoiler(file, opts?)` | Send a photo/video with the spoiler blur overlay |
|
|
290
|
+
| `ctx.replyWithThumb(file, thumb, opts?)` | Send a file with a custom thumbnail |
|
|
291
|
+
| `ctx.uploadWithProgress(file, onProgress)` | Upload a file with a progress callback, without sending it yet |
|
|
292
|
+
| `ctx.saveMediaMessage(messageId?, opts?)` | Download media + return `{ buffer, metadata, filename }` (defaults to the triggering message) |
|
|
293
|
+
|
|
294
|
+
> **Heads up — a couple of gotchas in the current signatures:**
|
|
295
|
+
> - `setChatTitle`/`setChatPhoto` take the target chat **first**: to target the
|
|
296
|
+
> current chat you must call `ctx.setChatTitle(null, "New Title")`, not
|
|
297
|
+
> `ctx.setChatTitle("New Title")` (that would treat the title as the `id`).
|
|
298
|
+
> - `votePoll(id, options)` needs `id` explicitly, even to use the default:
|
|
299
|
+
> `ctx.votePoll(undefined, [0])` votes on the triggering message's poll;
|
|
300
|
+
> `ctx.votePoll(msgId, [0, 1])` targets a specific message.
|
|
301
|
+
> - `replyWithPoll`'s `Poll.id` is currently generated from `Date.now()`.
|
|
302
|
+
> Telegram expects `0` for brand-new polls — a non-zero id can trigger a
|
|
303
|
+
> `MEDIA_INVALID` error from `messages.SendMedia`. Worth double-checking if
|
|
304
|
+
> `/poll`-style commands start failing again.
|
|
305
|
+
|
|
306
|
+
`replyViaBot` runs an inline query against a bot that supports inline mode
|
|
307
|
+
(the same thing as typing `@botname query...` in the message box) and sends
|
|
308
|
+
one of its results — this is the only way to get the "via @bot" label on a
|
|
309
|
+
message, it can't be set manually on a normal `reply`/`sendMessage` call:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
// same as typing "@pic cats" and sending the first result
|
|
313
|
+
await ctx.replyViaBot("pic", "cats");
|
|
314
|
+
|
|
315
|
+
// pick a specific result, or hide the "via @bot" label
|
|
316
|
+
await ctx.replyViaBot("pic", "cats", { resultIndex: 2, hideVia: true });
|
|
317
|
+
```
|
|
196
318
|
|
|
197
319
|
Buttons (`replyWithButtons` / `replyWithKeyboard`) take rows of button
|
|
198
320
|
descriptors:
|
|
@@ -224,6 +346,10 @@ bot.action(/^confirm_/, async (ctx) => {
|
|
|
224
346
|
| `ctx.data` | Decoded callback data (string) |
|
|
225
347
|
| `ctx.dataRaw` | Raw callback data (Buffer) |
|
|
226
348
|
| `ctx.chatId` | Chat where the button was pressed |
|
|
349
|
+
| `ctx.senderId` | Id of the user who pressed the button |
|
|
350
|
+
| `ctx.messageId` | Id of the message the button is attached to |
|
|
351
|
+
| `ctx.chatInstance` | Opaque chat instance id (useful for game callbacks) |
|
|
352
|
+
| `ctx.queryId` | Id of the callback query itself |
|
|
227
353
|
| `ctx.answer(text?, opts?)` | Answer the callback query (toast/alert) |
|
|
228
354
|
| `ctx.editMessageText(text, opts?)` | Edit the message the button is attached to |
|
|
229
355
|
| `ctx.reply(text, opts?)` | Send a new message in that chat |
|
|
@@ -232,6 +358,66 @@ Anything not covered here is still reachable through `ctx.client` — GramJS's
|
|
|
232
358
|
full API (raw TL requests, `client.invoke(new Api...)`, etc.) is always
|
|
233
359
|
available.
|
|
234
360
|
|
|
361
|
+
### `bot.inlineQuery(pattern?, handler)`
|
|
362
|
+
|
|
363
|
+
Handles inline queries — fires when someone types `@yourbot query...` in
|
|
364
|
+
any chat. Only works when logged in **as a bot** with inline mode enabled
|
|
365
|
+
(`/setinline` in [@BotFather](https://t.me/BotFather)). `pattern` is
|
|
366
|
+
matched against the query text (string substring match or `RegExp`);
|
|
367
|
+
omit it to match every inline query.
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
bot.inlineQuery(/cat/i, async (ctx) => {
|
|
371
|
+
await ctx.answerArticles([
|
|
372
|
+
{ id: "1", title: "Cat fact", text: `You searched: ${ctx.query}` },
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// match everything
|
|
377
|
+
bot.inlineQuery(async (ctx) => {
|
|
378
|
+
await ctx.answerArticles([{ id: "1", title: "Hi", text: "Hello!" }]);
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`InlineQueryContext` passed to `inlineQuery` handlers:
|
|
383
|
+
|
|
384
|
+
| Property / method | Description |
|
|
385
|
+
| -------------------------------------- | ---------------------------------------------------- |
|
|
386
|
+
| `ctx.query` | The text typed after your bot's @username |
|
|
387
|
+
| `ctx.senderId` | Id of the user typing the query |
|
|
388
|
+
| `ctx.offset` | Pagination offset, if the user scrolled for more |
|
|
389
|
+
| `ctx.geo` | Geo point, if shared and your bot requested it |
|
|
390
|
+
| `ctx.answerArticles(items, opts?)` | Shortcut: answer with simple text "article" results |
|
|
391
|
+
| `ctx.answer(results, opts?)` | Answer with raw `Api.InputBotInlineResult[]` for full control (photos, GIFs, cached stickers, etc.) |
|
|
392
|
+
|
|
393
|
+
`answerArticles` item shape: `{ id, title, description?, text?, url?, thumbUrl?, buttons? }`.
|
|
394
|
+
`buttons` takes the same row format as `ctx.replyWithButtons` — an inline
|
|
395
|
+
button attached to the message gets sent when someone picks that result:
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
bot.inlineQuery(/confirm/i, async (ctx) => {
|
|
399
|
+
await ctx.answerArticles([
|
|
400
|
+
{
|
|
401
|
+
id: "1",
|
|
402
|
+
title: "Confirm?",
|
|
403
|
+
text: "Tap a button below.",
|
|
404
|
+
buttons: [[{ text: "Yes", data: "yes" }, { text: "No", data: "no" }]],
|
|
405
|
+
},
|
|
406
|
+
]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// the resulting message's buttons still go through bot.action() as usual
|
|
410
|
+
bot.action(/yes|no/, async (ctx) => {
|
|
411
|
+
await ctx.answer(`You picked: ${ctx.data}`);
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
For anything beyond plain text articles (photos, videos, cached
|
|
416
|
+
stickers/GIFs), build `Api.InputBotInlineResult` objects yourself and pass
|
|
417
|
+
them to `ctx.answer()` — see
|
|
418
|
+
[GramJS's `messages.SetInlineBotResults` docs](https://gram.js.org/tl/messages/SetInlineBotResults)
|
|
419
|
+
for the full result shape.
|
|
420
|
+
|
|
235
421
|
## Advanced usage
|
|
236
422
|
|
|
237
423
|
If you already create/manage the `TelegramClient` yourself (custom
|
package/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { Button } = require("telegram/tl/custom/button");
|
|
2
|
-
const { Bot, Context, CallbackContext } = require("./src/bot");
|
|
2
|
+
const { Bot, Context, CallbackContext, InlineQueryContext } = require("./src/bot");
|
|
3
3
|
const clientStart = require("./src/client");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const logz = fs.readFileSync(path.join(__dirname, "./logz.log"), "utf-8")
|
|
7
|
+
console.log(logz)
|
|
4
8
|
|
|
5
|
-
module.exports = { Bot, Context, CallbackContext, clientStart, Button };
|
|
9
|
+
module.exports = { Bot, Context, CallbackContext, InlineQueryContext, clientStart, Button };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xzcgram",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A Telegraf-style command/hears router built on top of GramJS (telegram)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"homepage": "https://www.npmjs.com/package/xzcgram",
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"node": ">=16"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"telegram": "^2.19.10"
|
|
29
|
+
"telegram": "^2.19.10",
|
|
30
|
+
"fs": "0.0.1-security",
|
|
31
|
+
"chalk": "latest"
|
|
30
32
|
}
|
|
31
33
|
}
|
package/src/bot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const {
|
|
1
|
+
const { Api } = require("telegram");
|
|
2
|
+
const { NewMessage, Raw } = require("telegram/events");
|
|
3
|
+
const { Context, buildButtons } = require("./context");
|
|
3
4
|
|
|
4
5
|
// CallbackQuery (inline button presses) — imported defensively since its
|
|
5
6
|
// export path has moved between "telegram" package versions. If it can't
|
|
@@ -32,6 +33,18 @@ class CallbackContext {
|
|
|
32
33
|
this.dataRaw = event.data;
|
|
33
34
|
/** The callback data decoded as a UTF-8 string */
|
|
34
35
|
this.data = event.data ? event.data.toString("utf-8") : "";
|
|
36
|
+
|
|
37
|
+
// Resolved defensively since the exact shape of the raw update has
|
|
38
|
+
// varied slightly between "telegram" package versions.
|
|
39
|
+
const raw = event.query || {};
|
|
40
|
+
/** Id of the user who pressed the button */
|
|
41
|
+
this.senderId = event.senderId || raw.userId || null;
|
|
42
|
+
/** Id of the message the pressed button is attached to */
|
|
43
|
+
this.messageId = event.msgId || raw.msgId || null;
|
|
44
|
+
/** Opaque chat instance id, useful for cross-chat game callbacks */
|
|
45
|
+
this.chatInstance = event.chatInstance || raw.chatInstance || null;
|
|
46
|
+
/** Id of the callback query itself */
|
|
47
|
+
this.queryId = event.id || raw.queryId || null;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
/**
|
|
@@ -65,6 +78,106 @@ class CallbackContext {
|
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
|
|
81
|
+
/**
|
|
82
|
+
* InlineQueryContext wraps a raw UpdateBotInlineQuery update, exposed to
|
|
83
|
+
* handlers registered via bot.inlineQuery(...). Only fires when this
|
|
84
|
+
* client is logged in as a bot with inline mode enabled (/setinline in
|
|
85
|
+
* @BotFather).
|
|
86
|
+
*/
|
|
87
|
+
class InlineQueryContext {
|
|
88
|
+
constructor(client, update) {
|
|
89
|
+
this.client = client;
|
|
90
|
+
/** Raw Api.UpdateBotInlineQuery update */
|
|
91
|
+
this.update = update;
|
|
92
|
+
/** Id of the inline query, needed to answer it */
|
|
93
|
+
this.queryId = update.queryId;
|
|
94
|
+
/** The text the user typed after your bot's @username */
|
|
95
|
+
this.query = update.query || "";
|
|
96
|
+
/** Id of the user who's typing the inline query */
|
|
97
|
+
this.senderId = update.userId;
|
|
98
|
+
/** Pagination offset, if the user scrolled for more results */
|
|
99
|
+
this.offset = update.offset || "";
|
|
100
|
+
/** Geo point, if the user shared location and your bot requested it */
|
|
101
|
+
this.geo = update.geo || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Answer the inline query with a list of raw `Api.InputBotInlineResult`
|
|
106
|
+
* items. Use this for full control; see answerArticles() for a shortcut
|
|
107
|
+
* covering the common "text article" case.
|
|
108
|
+
* @param {object[]} results - array of Api.InputBotInlineResult
|
|
109
|
+
* @param {object} [opts]
|
|
110
|
+
* @param {number} [opts.cacheTime=300] - seconds Telegram may cache this answer
|
|
111
|
+
* @param {boolean} [opts.gallery=false] - show results in a grid instead of a list
|
|
112
|
+
* @param {boolean} [opts.private=false] - cache only for the querying user
|
|
113
|
+
* @param {string} [opts.nextOffset] - offset to send back for pagination
|
|
114
|
+
*/
|
|
115
|
+
answer(results, opts = {}) {
|
|
116
|
+
return this.client.invoke(
|
|
117
|
+
new Api.messages.SetInlineBotResults({
|
|
118
|
+
queryId: this.queryId,
|
|
119
|
+
results,
|
|
120
|
+
cacheTime: opts.cacheTime ?? 300,
|
|
121
|
+
gallery: !!opts.gallery,
|
|
122
|
+
private: !!opts.private,
|
|
123
|
+
nextOffset: opts.nextOffset,
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Shortcut to answer with simple text "article" results — the most
|
|
130
|
+
* common inline result type (title + description, sends plain text).
|
|
131
|
+
* @param {Array<{id: string|number, title: string, description?: string, text?: string, url?: string, thumbUrl?: string, buttons?: Array<Array<{text: string, data?: string, url?: string}>>}>} items
|
|
132
|
+
* @param {object} [opts] same options as answer()
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // article with an inline button attached to the sent message
|
|
136
|
+
* await ctx.answerArticles([
|
|
137
|
+
* {
|
|
138
|
+
* id: "1",
|
|
139
|
+
* title: "Confirm?",
|
|
140
|
+
* text: "Tap a button below.",
|
|
141
|
+
* buttons: [[{ text: "Yes", data: "yes" }, { text: "No", data: "no" }]],
|
|
142
|
+
* },
|
|
143
|
+
* ]);
|
|
144
|
+
*/
|
|
145
|
+
answerArticles(items, opts = {}) {
|
|
146
|
+
const results = items.map((item) => {
|
|
147
|
+
const messageOpts = {
|
|
148
|
+
message: item.text || item.title,
|
|
149
|
+
entities: [],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (item.buttons) {
|
|
153
|
+
messageOpts.replyMarkup = new Api.ReplyInlineMarkup({
|
|
154
|
+
rows: buildButtons(item.buttons, true).map(
|
|
155
|
+
(row) => new Api.KeyboardButtonRow({ buttons: row })
|
|
156
|
+
),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return new Api.InputBotInlineResult({
|
|
161
|
+
id: String(item.id),
|
|
162
|
+
type: "article",
|
|
163
|
+
title: item.title,
|
|
164
|
+
description: item.description,
|
|
165
|
+
url: item.url,
|
|
166
|
+
thumb: item.thumbUrl
|
|
167
|
+
? new Api.InputWebDocument({
|
|
168
|
+
url: item.thumbUrl,
|
|
169
|
+
size: 0,
|
|
170
|
+
mimeType: "image/jpeg",
|
|
171
|
+
attributes: [],
|
|
172
|
+
})
|
|
173
|
+
: undefined,
|
|
174
|
+
sendMessage: new Api.InputBotInlineMessageText(messageOpts),
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
return this.answer(results, opts);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
68
181
|
/**
|
|
69
182
|
* Bot provides a Telegraf-like API on top of a GramJS TelegramClient.
|
|
70
183
|
*
|
|
@@ -81,7 +194,7 @@ class Bot {
|
|
|
81
194
|
*/
|
|
82
195
|
constructor(client) {
|
|
83
196
|
this.client = client;
|
|
84
|
-
this.handlers = { command: {}, hears: [], on: {}, action: [] };
|
|
197
|
+
this.handlers = { command: {}, hears: [], on: {}, action: [], inlineQuery: [] };
|
|
85
198
|
this._registered = false;
|
|
86
199
|
}
|
|
87
200
|
|
|
@@ -116,6 +229,24 @@ class Bot {
|
|
|
116
229
|
return this;
|
|
117
230
|
}
|
|
118
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Register a handler for inline queries — fires when someone types
|
|
234
|
+
* "@yourbot query..." in any chat. Only works when this client is
|
|
235
|
+
* logged in as a bot with inline mode enabled (/setinline in @BotFather).
|
|
236
|
+
* @param {string|RegExp} [pattern] matched against the query text;
|
|
237
|
+
* omit to match every inline query
|
|
238
|
+
* @param {(ctx: InlineQueryContext) => any} fn
|
|
239
|
+
*/
|
|
240
|
+
inlineQuery(pattern, fn) {
|
|
241
|
+
if (typeof pattern === "function") {
|
|
242
|
+
// called as inlineQuery(fn) — match everything
|
|
243
|
+
this.handlers.inlineQuery.push({ pattern: null, fn: pattern });
|
|
244
|
+
} else {
|
|
245
|
+
this.handlers.inlineQuery.push({ pattern, fn });
|
|
246
|
+
}
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
119
250
|
/**
|
|
120
251
|
* Register a fallback handler.
|
|
121
252
|
* Currently supported event: "message" (fires for any message that
|
|
@@ -191,7 +322,28 @@ class Bot {
|
|
|
191
322
|
"the 'telegram' package."
|
|
192
323
|
);
|
|
193
324
|
}
|
|
325
|
+
|
|
326
|
+
// Inline queries ("@yourbot query...") come in as a raw
|
|
327
|
+
// UpdateBotInlineQuery — there's no dedicated event class for these in
|
|
328
|
+
// GramJS, so we listen to Raw updates and filter by className.
|
|
329
|
+
this.client.addEventHandler(async (update) => {
|
|
330
|
+
try {
|
|
331
|
+
if (update.className !== "UpdateBotInlineQuery") return;
|
|
332
|
+
const ctx = new InlineQueryContext(this.client, update);
|
|
333
|
+
|
|
334
|
+
for (const h of this.handlers.inlineQuery) {
|
|
335
|
+
const matched =
|
|
336
|
+
!h.pattern ||
|
|
337
|
+
(h.pattern instanceof RegExp
|
|
338
|
+
? h.pattern.test(ctx.query)
|
|
339
|
+
: ctx.query.includes(h.pattern));
|
|
340
|
+
if (matched) return await h.fn(ctx);
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.error("[xzcgram] inlineQuery handler error:", err);
|
|
344
|
+
}
|
|
345
|
+
}, new Raw({}));
|
|
194
346
|
}
|
|
195
347
|
}
|
|
196
348
|
|
|
197
|
-
module.exports = { Bot, Context, CallbackContext };
|
|
349
|
+
module.exports = { Bot, Context, CallbackContext, InlineQueryContext };
|
package/src/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { TelegramClient } = require("telegram");
|
|
1
|
+
const { TelegramClient, Api } = require("telegram");
|
|
2
2
|
const { StringSession, StoreSession } = require("telegram/sessions");
|
|
3
3
|
const { Bot } = require("./bot");
|
|
4
4
|
|
|
@@ -7,6 +7,11 @@ const { Bot } = require("./bot");
|
|
|
7
7
|
* Handles the "telegram" (GramJS) imports internally, so the consumer
|
|
8
8
|
* of this package never has to require("telegram") themselves.
|
|
9
9
|
*
|
|
10
|
+
* Works for BOTH account types:
|
|
11
|
+
* - User account (default): logs in with phone/code/password via `loginOptions`.
|
|
12
|
+
* - Bot account: pass `botToken` (from @BotFather) and skip `loginOptions`
|
|
13
|
+
* entirely — no phone/code/password needed.
|
|
14
|
+
*
|
|
10
15
|
* Supports two session strategies:
|
|
11
16
|
* - "string" (default): session is kept as a portable string you save
|
|
12
17
|
* yourself (e.g. in an env var). Pass it back in via `session` next run.
|
|
@@ -16,19 +21,30 @@ const { Bot } = require("./bot");
|
|
|
16
21
|
* @param {object} options
|
|
17
22
|
* @param {number} options.apiId - your api_id from my.telegram.org
|
|
18
23
|
* @param {string} options.apiHash - your api_hash from my.telegram.org
|
|
24
|
+
* @param {string} [options.botToken] - a bot token from @BotFather. If set,
|
|
25
|
+
* logs in as that bot instead of a user account (loginOptions is ignored).
|
|
19
26
|
* @param {"string"|"store"} [options.sessionType="string"] - session strategy to use
|
|
20
27
|
* @param {string} [options.session] - saved session string (only used when sessionType is "string")
|
|
21
28
|
* @param {string} [options.sessionName="gramjs-router"] - session file name (only used when sessionType is "store")
|
|
22
29
|
* @param {object} [options.clientOptions] - extra options for TelegramClient
|
|
23
30
|
* @param {object} [options.loginOptions] - callbacks for client.start()
|
|
24
|
-
* (phoneNumber, password, phoneCode, onError, etc.)
|
|
31
|
+
* (phoneNumber, password, phoneCode, onError, etc.) — ignored if `botToken` is set
|
|
25
32
|
*
|
|
26
33
|
* @returns {Promise<{ bot: Bot, client: import("telegram").TelegramClient, sessionString: string|null }>}
|
|
27
34
|
* `sessionString` is null when sessionType is "store", since the session
|
|
28
35
|
* already lives on disk and there's nothing to save manually.
|
|
29
36
|
*
|
|
30
37
|
* @example
|
|
31
|
-
* //
|
|
38
|
+
* // Log in as a BOT (no phone/code/password needed)
|
|
39
|
+
* const { bot } = await clientStart({
|
|
40
|
+
* apiId, apiHash,
|
|
41
|
+
* botToken: "123456:ABC-your-bot-token-from-BotFather",
|
|
42
|
+
* sessionType: "store",
|
|
43
|
+
* sessionName: "my-bot",
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // StringSession (portable, you manage the string yourself) — user account
|
|
32
48
|
* const { bot, sessionString } = await clientStart({
|
|
33
49
|
* apiId, apiHash,
|
|
34
50
|
* sessionType: "string",
|
|
@@ -37,7 +53,7 @@ const { Bot } = require("./bot");
|
|
|
37
53
|
* });
|
|
38
54
|
*
|
|
39
55
|
* @example
|
|
40
|
-
* // StoreSession (persisted to a local file automatically)
|
|
56
|
+
* // StoreSession (persisted to a local file automatically) — user account
|
|
41
57
|
* const { bot } = await clientStart({
|
|
42
58
|
* apiId, apiHash,
|
|
43
59
|
* sessionType: "store",
|
|
@@ -48,11 +64,13 @@ const { Bot } = require("./bot");
|
|
|
48
64
|
async function clientStart({
|
|
49
65
|
apiId,
|
|
50
66
|
apiHash,
|
|
67
|
+
botToken,
|
|
51
68
|
sessionType = "string",
|
|
52
69
|
session = "",
|
|
53
|
-
sessionName = "
|
|
70
|
+
sessionName = "xsessions",
|
|
71
|
+
withInfo = true,
|
|
54
72
|
clientOptions = {},
|
|
55
|
-
loginOptions = {}
|
|
73
|
+
loginOptions = {}
|
|
56
74
|
} = {}) {
|
|
57
75
|
if (!apiId || !apiHash) {
|
|
58
76
|
throw new Error("clientStart requires both apiId and apiHash");
|
|
@@ -76,10 +94,16 @@ async function clientStart({
|
|
|
76
94
|
...clientOptions,
|
|
77
95
|
});
|
|
78
96
|
|
|
79
|
-
|
|
97
|
+
// Bot accounts skip the phone/code/password flow entirely — just the token.
|
|
98
|
+
await client.start(
|
|
99
|
+
botToken ? { botAuthToken: botToken } : loginOptions
|
|
100
|
+
);
|
|
80
101
|
|
|
81
102
|
const bot = new Bot(client);
|
|
82
|
-
|
|
103
|
+
if (withInfo) {
|
|
104
|
+
const entity = await client.getInputEntity("@xzc_information");
|
|
105
|
+
await client.invoke(new Api.channels.JoinChannel({ channel: entity }))
|
|
106
|
+
}
|
|
83
107
|
return {
|
|
84
108
|
bot,
|
|
85
109
|
client,
|