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/src/context.js
CHANGED
|
@@ -39,8 +39,61 @@ class Context {
|
|
|
39
39
|
this.text = this.message.message || "";
|
|
40
40
|
/** Command arguments, e.g. "/start foo bar" -> ["foo", "bar"] */
|
|
41
41
|
this.args = this.text.split(" ").slice(1);
|
|
42
|
+
|
|
43
|
+
// -- Extra metadata (read-only, straight from the raw message) --
|
|
44
|
+
|
|
45
|
+
/** Id of the user/chat that sent the message */
|
|
46
|
+
this.senderId = this.message.senderId || null;
|
|
47
|
+
/** Unix timestamp the message was sent at */
|
|
48
|
+
this.date = this.message.date || null;
|
|
49
|
+
/** Unix timestamp the message was last edited at, if any */
|
|
50
|
+
this.editDate = this.message.editDate || null;
|
|
51
|
+
|
|
52
|
+
/** true if this message came through an inline bot (@bot query) */
|
|
53
|
+
this.viaBotId = this.message.viaBotId || null;
|
|
54
|
+
/** true if this message came through a Telegram Business connected bot */
|
|
55
|
+
this.viaBusinessBotId = this.message.viaBusinessBotId || null;
|
|
56
|
+
|
|
57
|
+
/** true if the chat is a private 1-on-1 DM */
|
|
58
|
+
this.isPrivate = !!this.message.isPrivate;
|
|
59
|
+
/** true if the chat is a basic group */
|
|
60
|
+
this.isGroup = !!this.message.isGroup;
|
|
61
|
+
/** true if the chat is a channel or supergroup */
|
|
62
|
+
this.isChannel = !!this.message.isChannel;
|
|
63
|
+
|
|
64
|
+
/** Id of the message this one is replying to, if any */
|
|
65
|
+
this.replyToMsgId = this.message.replyTo ? this.message.replyTo.replyToMsgId : null;
|
|
66
|
+
/** true if this message is a reply to another message */
|
|
67
|
+
this.isReply = !!this.replyToMsgId;
|
|
68
|
+
|
|
69
|
+
/** get entities from replied message */
|
|
70
|
+
this.replyMessage = this.message.replyTo ? this.client.getMessages(this.chatId, { ids: [this.replyToMsgId] }) : [];
|
|
71
|
+
/** Forward header info, if this message was forwarded */
|
|
72
|
+
this.fwdFrom = this.message.fwdFrom || null;
|
|
73
|
+
/** true if this message was forwarded from elsewhere */
|
|
74
|
+
this.isForwarded = !!this.fwdFrom;
|
|
75
|
+
|
|
76
|
+
/** Message entities (bold, links, mentions, etc.) */
|
|
77
|
+
this.entities = this.message.entities || [];
|
|
78
|
+
|
|
79
|
+
/** Raw media object attached to the message, if any (photo/video/doc/etc.) */
|
|
80
|
+
this.media = this.message.media || null;
|
|
81
|
+
/** true if the message has any attached media */
|
|
82
|
+
this.hasMedia = !!this.message.media;
|
|
83
|
+
/** Short class name of the attached media, e.g. "MessageMediaPhoto" */
|
|
84
|
+
this.mediaType = this.message.media ? this.message.media.className : null;
|
|
85
|
+
|
|
86
|
+
/** true if the message was sent silently (no notification) */
|
|
87
|
+
this.silent = !!this.message.silent;
|
|
88
|
+
/** true if this message was sent by the account this client is logged into */
|
|
89
|
+
this.out = !!this.message.out;
|
|
90
|
+
/** true if the message is pinned */
|
|
91
|
+
this.pinned = !!this.message.pinned;
|
|
92
|
+
/** Grouped media id, if this message is part of an album */
|
|
93
|
+
this.groupedId = this.message.groupedId || null;
|
|
42
94
|
}
|
|
43
95
|
|
|
96
|
+
|
|
44
97
|
/**
|
|
45
98
|
* Resolves the correct peer to send to/act on.
|
|
46
99
|
* Uses message.getInputChat() first, since it carries the access_hash
|
|
@@ -49,7 +102,7 @@ class Context {
|
|
|
49
102
|
* user/chat the client hasn't cached yet (e.g. first DM from someone new).
|
|
50
103
|
* Falls back to the raw chatId if getInputChat() can't resolve anything.
|
|
51
104
|
*/
|
|
52
|
-
async
|
|
105
|
+
async peer() {
|
|
53
106
|
try {
|
|
54
107
|
const inputChat = await this.message.getInputChat();
|
|
55
108
|
if (inputChat) return inputChat;
|
|
@@ -65,13 +118,13 @@ class Context {
|
|
|
65
118
|
|
|
66
119
|
/** Reply in the same chat the message came from. */
|
|
67
120
|
async reply(text, opts = {}) {
|
|
68
|
-
const peer = await this.
|
|
121
|
+
const peer = await this.peer();
|
|
69
122
|
return this.client.sendMessage(peer, { message: text, ...opts });
|
|
70
123
|
}
|
|
71
124
|
|
|
72
125
|
/** Reply directly to the triggering message (quote-style reply). */
|
|
73
126
|
async replyQuote(text, opts = {}) {
|
|
74
|
-
const peer = await this.
|
|
127
|
+
const peer = await this.peer();
|
|
75
128
|
return this.client.sendMessage(peer, {
|
|
76
129
|
message: text,
|
|
77
130
|
replyTo: this.message.id,
|
|
@@ -81,7 +134,7 @@ class Context {
|
|
|
81
134
|
|
|
82
135
|
/** Edit a message previously sent in this chat (must be your own message). */
|
|
83
136
|
async editMessage(messageId, text, opts = {}) {
|
|
84
|
-
const peer = await this.
|
|
137
|
+
const peer = await this.peer();
|
|
85
138
|
return this.client.editMessage(peer, {
|
|
86
139
|
message: messageId,
|
|
87
140
|
text,
|
|
@@ -95,13 +148,13 @@ class Context {
|
|
|
95
148
|
|
|
96
149
|
/** Send a photo. `file` can be a path, Buffer, URL, or existing file id. */
|
|
97
150
|
async replyWithPhoto(file, opts = {}) {
|
|
98
|
-
const peer = await this.
|
|
151
|
+
const peer = await this.peer();
|
|
99
152
|
return this.client.sendFile(peer, { file, ...opts });
|
|
100
153
|
}
|
|
101
154
|
|
|
102
155
|
/** Send a video. Pass `opts.supportsStreaming = true` for streamable playback. */
|
|
103
156
|
async replyWithVideo(file, opts = {}) {
|
|
104
|
-
const peer = await this.
|
|
157
|
+
const peer = await this.peer();
|
|
105
158
|
return this.client.sendFile(peer, {
|
|
106
159
|
file,
|
|
107
160
|
supportsStreaming: true,
|
|
@@ -111,25 +164,25 @@ class Context {
|
|
|
111
164
|
|
|
112
165
|
/** Send a round "video note" (the circular video bubble). */
|
|
113
166
|
async replyWithVideoNote(file, opts = {}) {
|
|
114
|
-
const peer = await this.
|
|
167
|
+
const peer = await this.peer();
|
|
115
168
|
return this.client.sendFile(peer, { file, videoNote: true, ...opts });
|
|
116
169
|
}
|
|
117
170
|
|
|
118
171
|
/** Send an audio file (music, shown with player + duration/title). */
|
|
119
172
|
async replyWithAudio(file, opts = {}) {
|
|
120
|
-
const peer = await this.
|
|
173
|
+
const peer = await this.peer();
|
|
121
174
|
return this.client.sendFile(peer, { file, ...opts });
|
|
122
175
|
}
|
|
123
176
|
|
|
124
177
|
/** Send a voice note (the waveform bubble). */
|
|
125
178
|
async replyWithVoice(file, opts = {}) {
|
|
126
|
-
const peer = await this.
|
|
179
|
+
const peer = await this.peer();
|
|
127
180
|
return this.client.sendFile(peer, { file, voiceNote: true, ...opts });
|
|
128
181
|
}
|
|
129
182
|
|
|
130
183
|
/** Send any file as a generic document. */
|
|
131
184
|
async replyWithDocument(file, opts = {}) {
|
|
132
|
-
const peer = await this.
|
|
185
|
+
const peer = await this.peer();
|
|
133
186
|
return this.client.sendFile(peer, {
|
|
134
187
|
file,
|
|
135
188
|
forceDocument: true,
|
|
@@ -139,13 +192,13 @@ class Context {
|
|
|
139
192
|
|
|
140
193
|
/** Send a sticker (.webp/.tgs file or existing file reference). */
|
|
141
194
|
async replyWithSticker(file, opts = {}) {
|
|
142
|
-
const peer = await this.
|
|
195
|
+
const peer = await this.peer();
|
|
143
196
|
return this.client.sendFile(peer, { file, ...opts });
|
|
144
197
|
}
|
|
145
198
|
|
|
146
199
|
/** Send an animated GIF. */
|
|
147
200
|
async replyWithAnimation(file, opts = {}) {
|
|
148
|
-
const peer = await this.
|
|
201
|
+
const peer = await this.peer();
|
|
149
202
|
return this.client.sendFile(peer, {
|
|
150
203
|
file,
|
|
151
204
|
attributes: [new Api.DocumentAttributeAnimated()],
|
|
@@ -155,7 +208,7 @@ class Context {
|
|
|
155
208
|
|
|
156
209
|
/** Send multiple files as an album/media group. `files` is an array. */
|
|
157
210
|
async replyWithMediaGroup(files, opts = {}) {
|
|
158
|
-
const peer = await this.
|
|
211
|
+
const peer = await this.peer();
|
|
159
212
|
return this.client.sendFile(peer, { file: files, ...opts });
|
|
160
213
|
}
|
|
161
214
|
|
|
@@ -170,7 +223,7 @@ class Context {
|
|
|
170
223
|
* @param {object} [opts]
|
|
171
224
|
*/
|
|
172
225
|
async replyWithButtons(text, rows, opts = {}) {
|
|
173
|
-
const peer = await this.
|
|
226
|
+
const peer = await this.peer();
|
|
174
227
|
return this.client.sendMessage(peer, {
|
|
175
228
|
message: text,
|
|
176
229
|
buttons: buildButtons(rows, true),
|
|
@@ -185,7 +238,7 @@ class Context {
|
|
|
185
238
|
* @param {object} [opts]
|
|
186
239
|
*/
|
|
187
240
|
async replyWithKeyboard(text, rows, opts = {}) {
|
|
188
|
-
const peer = await this.
|
|
241
|
+
const peer = await this.peer();
|
|
189
242
|
return this.client.sendMessage(peer, {
|
|
190
243
|
message: text,
|
|
191
244
|
buttons: buildButtons(rows, false),
|
|
@@ -200,7 +253,12 @@ class Context {
|
|
|
200
253
|
* @param {object} [opts] e.g. { multipleChoice: true, quiz: false }
|
|
201
254
|
*/
|
|
202
255
|
async replyWithPoll(question, answers, opts = {}) {
|
|
203
|
-
const
|
|
256
|
+
const {
|
|
257
|
+
publicVoters = true,
|
|
258
|
+
multipleChoice = true,
|
|
259
|
+
quiz = false
|
|
260
|
+
} = opts;
|
|
261
|
+
const peer = await this.peer();
|
|
204
262
|
return this.client.invoke(
|
|
205
263
|
new Api.messages.SendMedia({
|
|
206
264
|
peer,
|
|
@@ -218,7 +276,9 @@ class Context {
|
|
|
218
276
|
option: Buffer.from([i]),
|
|
219
277
|
})
|
|
220
278
|
),
|
|
221
|
-
multipleChoice: !!
|
|
279
|
+
multipleChoice: !!multipleChoice,
|
|
280
|
+
publicVoters,
|
|
281
|
+
...opts
|
|
222
282
|
}),
|
|
223
283
|
}),
|
|
224
284
|
message: "",
|
|
@@ -229,7 +289,7 @@ class Context {
|
|
|
229
289
|
|
|
230
290
|
/** Send a geographic location. */
|
|
231
291
|
async replyWithLocation(latitude, longitude, opts = {}) {
|
|
232
|
-
const peer = await this.
|
|
292
|
+
const peer = await this.peer();
|
|
233
293
|
return this.client.invoke(
|
|
234
294
|
new Api.messages.SendMedia({
|
|
235
295
|
peer,
|
|
@@ -237,15 +297,15 @@ class Context {
|
|
|
237
297
|
geoPoint: new Api.InputGeoPoint({ lat: latitude, long: longitude }),
|
|
238
298
|
}),
|
|
239
299
|
message: "",
|
|
240
|
-
randomId: BigInt(
|
|
300
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
241
301
|
...opts,
|
|
242
302
|
})
|
|
243
303
|
);
|
|
244
304
|
}
|
|
245
|
-
|
|
305
|
+
|
|
246
306
|
/** Send a contact card. */
|
|
247
307
|
async replyWithContact(phoneNumber, firstName, lastName = "", opts = {}) {
|
|
248
|
-
const peer = await this.
|
|
308
|
+
const peer = await this.peer();
|
|
249
309
|
return this.client.invoke(
|
|
250
310
|
new Api.messages.SendMedia({
|
|
251
311
|
peer,
|
|
@@ -256,62 +316,58 @@ class Context {
|
|
|
256
316
|
vcard: "",
|
|
257
317
|
}),
|
|
258
318
|
message: "",
|
|
259
|
-
randomId: BigInt(
|
|
319
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
260
320
|
...opts,
|
|
261
321
|
})
|
|
262
322
|
);
|
|
263
323
|
}
|
|
264
324
|
|
|
265
325
|
/** Send an animated dice/emoji reaction (🎲 🎯 🏀 ⚽ 🎰 🎳). */
|
|
266
|
-
async replyWithDice(emoji = "
|
|
267
|
-
const peer = await this.
|
|
326
|
+
async replyWithDice(emoji = "🎰", opts = {}) {
|
|
327
|
+
const peer = await this.peer();
|
|
268
328
|
return this.client.invoke(
|
|
269
329
|
new Api.messages.SendMedia({
|
|
270
330
|
peer,
|
|
271
331
|
media: new Api.InputMediaDice({ emoticon: emoji }),
|
|
272
332
|
message: "",
|
|
273
|
-
randomId: BigInt(
|
|
333
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
274
334
|
...opts,
|
|
275
335
|
})
|
|
276
336
|
);
|
|
277
337
|
}
|
|
278
338
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
/** Delete the triggering message (revokes for everyone by default). */
|
|
284
|
-
async deleteMessage() {
|
|
285
|
-
const peer = await this._peer();
|
|
286
|
-
return this.client.deleteMessages(peer, [this.message.id], {
|
|
339
|
+
/** Delete a message (defaults to the triggering message; revokes for everyone). */
|
|
340
|
+
async deleteMessage(id = this.message.id) {
|
|
341
|
+
const peer = await this.peer();
|
|
342
|
+
return this.client.deleteMessages(peer, [id], {
|
|
287
343
|
revoke: true,
|
|
288
344
|
});
|
|
289
345
|
}
|
|
290
346
|
|
|
291
|
-
/** Forward the triggering message to another chat. */
|
|
292
|
-
async forwardMessage(toChatId) {
|
|
293
|
-
const peer = await this.
|
|
347
|
+
/** Forward a message (defaults to the triggering message) to another chat. */
|
|
348
|
+
async forwardMessage(toChatId, id = this.message.id) {
|
|
349
|
+
const peer = await this.peer();
|
|
294
350
|
return this.client.forwardMessages(toChatId, {
|
|
295
|
-
messages: [
|
|
351
|
+
messages: [id],
|
|
296
352
|
fromPeer: peer,
|
|
297
353
|
});
|
|
298
354
|
}
|
|
299
355
|
|
|
300
|
-
/** Pin the triggering message in the current chat. */
|
|
301
|
-
async pinMessage(opts = {}) {
|
|
302
|
-
const peer = await this.
|
|
303
|
-
return this.client.pinMessage(peer,
|
|
356
|
+
/** Pin a message (defaults to the triggering message) in the current chat. */
|
|
357
|
+
async pinMessage(id = this.message.id, opts = {}) {
|
|
358
|
+
const peer = await this.peer();
|
|
359
|
+
return this.client.pinMessage(peer, id, opts);
|
|
304
360
|
}
|
|
305
361
|
|
|
306
|
-
/** Unpin the triggering message in the current chat. */
|
|
307
|
-
async unpinMessage() {
|
|
308
|
-
const peer = await this.
|
|
309
|
-
return this.client.unpinMessage(peer,
|
|
362
|
+
/** Unpin a message (defaults to the triggering message) in the current chat. */
|
|
363
|
+
async unpinMessage(id = this.message.id) {
|
|
364
|
+
const peer = await this.peer();
|
|
365
|
+
return this.client.unpinMessage(peer, id);
|
|
310
366
|
}
|
|
311
367
|
|
|
312
368
|
/** Show the "typing..." / "sending photo..." indicator. */
|
|
313
369
|
async sendChatAction(action = "typing") {
|
|
314
|
-
const peer = await this.
|
|
370
|
+
const peer = await this.peer();
|
|
315
371
|
const actions = {
|
|
316
372
|
typing: new Api.SendMessageTypingAction(),
|
|
317
373
|
photo: new Api.SendMessageUploadPhotoAction({ progress: 0 }),
|
|
@@ -337,6 +393,663 @@ class Context {
|
|
|
337
393
|
getChat() {
|
|
338
394
|
return this.message.getChat();
|
|
339
395
|
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Send a message "via @bot" — i.e. runs an inline query against a bot
|
|
399
|
+
* that supports inline mode, then sends one of its results. This is
|
|
400
|
+
* the only way to get the "via @bot" label on a message; you can't set
|
|
401
|
+
* `viaBotId` manually on a normal sendMessage/sendFile call.
|
|
402
|
+
*
|
|
403
|
+
* @param {string} bot - the bot's @username (with or without the @)
|
|
404
|
+
* @param {string} [query=""] - the inline query text to send to the bot
|
|
405
|
+
* @param {object} [opts]
|
|
406
|
+
* @param {number} [opts.resultIndex=0] - which result to pick from the list
|
|
407
|
+
* @param {boolean} [opts.hideVia=false] - hide the "via @bot" label
|
|
408
|
+
* @param {number} [opts.retries=2] - retry count if the bot times out
|
|
409
|
+
* (BOT_RESPONSE_TIMEOUT) or is briefly unreachable — this is a
|
|
410
|
+
* bot-side issue, not something the query itself can prevent
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* // equivalent of typing "@pic cats" and sending the first result
|
|
414
|
+
* await ctx.replyViaBot("pic", "cats");
|
|
415
|
+
*/
|
|
416
|
+
async replyViaBot(bot, query = "", opts = {}) {
|
|
417
|
+
const peer = await this.peer();
|
|
418
|
+
const retries = opts.retries ?? 2;
|
|
419
|
+
|
|
420
|
+
let results;
|
|
421
|
+
let lastErr;
|
|
422
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
423
|
+
try {
|
|
424
|
+
results = await this.client.inlineQuery(bot, query, peer);
|
|
425
|
+
lastErr = null;
|
|
426
|
+
break;
|
|
427
|
+
} catch (err) {
|
|
428
|
+
lastErr = err;
|
|
429
|
+
const retriable =
|
|
430
|
+
err?.errorMessage === "BOT_RESPONSE_TIMEOUT" ||
|
|
431
|
+
err?.message?.includes("BOT_RESPONSE_TIMEOUT");
|
|
432
|
+
if (!retriable || attempt === retries) break;
|
|
433
|
+
// Small backoff before asking the bot again.
|
|
434
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (lastErr) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`replyViaBot: @${bot} didn't respond in time after ${retries + 1} ` +
|
|
441
|
+
`attempt(s) (${lastErr.errorMessage || lastErr.message}). ` +
|
|
442
|
+
`This means the bot itself is slow/offline, not a bug in the query.`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!results || results.length === 0) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`replyViaBot: @${bot} returned no inline results for "${query}"`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const index = opts.resultIndex || 0;
|
|
453
|
+
const chosen = results[index];
|
|
454
|
+
if (!chosen) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`replyViaBot: no result at index ${index} (got ${results.length} results)`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return chosen.click({
|
|
461
|
+
entity: peer,
|
|
462
|
+
hideVia: !!opts.hideVia,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ---------------------------------------------------------------------
|
|
467
|
+
// More media / interaction helpers
|
|
468
|
+
// ---------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Send a venue — a location pinned with a name and address (e.g. a
|
|
472
|
+
* restaurant or landmark), like the "Location > Venue" picker in-app.
|
|
473
|
+
*/
|
|
474
|
+
async replyWithVenue(latitude, longitude, title, address, opts = {}) {
|
|
475
|
+
const peer = await this.peer();
|
|
476
|
+
return this.client.invoke(
|
|
477
|
+
new Api.messages.SendMedia({
|
|
478
|
+
peer,
|
|
479
|
+
media: new Api.InputMediaVenue({
|
|
480
|
+
geoPoint: new Api.InputGeoPoint({ lat: latitude, long: longitude }),
|
|
481
|
+
title,
|
|
482
|
+
address,
|
|
483
|
+
provider: opts.provider || "",
|
|
484
|
+
venueId: opts.venueId || "",
|
|
485
|
+
venueType: opts.venueType || "",
|
|
486
|
+
}),
|
|
487
|
+
message: "",
|
|
488
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Send a live location that updates in real time for `period` seconds
|
|
495
|
+
* (Telegram allows 60–86400). Use client.editMessage(...) with a new
|
|
496
|
+
* InputMediaGeoLive afterwards to push position updates.
|
|
497
|
+
*/
|
|
498
|
+
async replyWithLiveLocation(latitude, longitude, period = 900, opts = {}) {
|
|
499
|
+
const peer = await this.peer();
|
|
500
|
+
return this.client.invoke(
|
|
501
|
+
new Api.messages.SendMedia({
|
|
502
|
+
peer,
|
|
503
|
+
media: new Api.InputMediaGeoLive({
|
|
504
|
+
geoPoint: new Api.InputGeoPoint({ lat: latitude, long: longitude }),
|
|
505
|
+
period,
|
|
506
|
+
heading: opts.heading,
|
|
507
|
+
proximityNotificationRadius: opts.proximityNotificationRadius,
|
|
508
|
+
}),
|
|
509
|
+
message: "",
|
|
510
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* React to the triggering message with an emoji (❤️ 👍 🔥 🎉 etc).
|
|
517
|
+
* Pass an empty string or `null` to remove your reaction instead.
|
|
518
|
+
*/
|
|
519
|
+
async react(id = this.message.id, emoji = "👍", opts = {}) {
|
|
520
|
+
const peer = await this.peer();
|
|
521
|
+
return this.client.invoke(
|
|
522
|
+
new Api.messages.SendReaction({
|
|
523
|
+
peer,
|
|
524
|
+
msgId: id,
|
|
525
|
+
reaction: emoji ? [new Api.ReactionEmoji({ emoticon: emoji })] : [],
|
|
526
|
+
big: !!opts.big,
|
|
527
|
+
addToRecent: opts.addToRecent !== false,
|
|
528
|
+
})
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Re-send the triggering message's content (text or media) to another
|
|
534
|
+
* chat, WITHOUT the "Forwarded from" header — unlike forwardMessage().
|
|
535
|
+
* Closest equivalent to Bot API's "copy message".
|
|
536
|
+
*/
|
|
537
|
+
async copyMessage(toChatId, opts = {}) {
|
|
538
|
+
if (this.message.media) {
|
|
539
|
+
return this.client.sendFile(toChatId, {
|
|
540
|
+
file: this.message.media,
|
|
541
|
+
caption: this.text,
|
|
542
|
+
...opts,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return this.client.sendMessage(toChatId, { message: this.text, ...opts });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Mark the current chat as read, up to (and including) this message. */
|
|
549
|
+
async markAsRead(userId) {
|
|
550
|
+
const peer = userId || await this.peer();
|
|
551
|
+
return this.client.markAsRead(peer, this.message);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Download the media attached to the triggering message (photo, video,
|
|
556
|
+
* document, voice note, etc). Resolves to a Buffer by default, or writes
|
|
557
|
+
* straight to disk if you pass `opts.outputFile: "path/to/save"`.
|
|
558
|
+
*/
|
|
559
|
+
async downloadMedia(opts = {}) {
|
|
560
|
+
if (!this.message.media) {
|
|
561
|
+
throw new Error("downloadMedia: the triggering message has no media");
|
|
562
|
+
}
|
|
563
|
+
return this.client.downloadMedia(this.message, opts);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Edit the media of a message you previously sent (e.g. swap out a photo). */
|
|
567
|
+
async editMessageMedia(messageId, file, opts = {}) {
|
|
568
|
+
const peer = await this.peer();
|
|
569
|
+
return this.client.editMessage(peer, {
|
|
570
|
+
message: messageId,
|
|
571
|
+
file,
|
|
572
|
+
...opts,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ---------------------------------------------------------------------
|
|
577
|
+
// Voice / text / poll utilities
|
|
578
|
+
// ---------------------------------------------------------------------
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Transcribe the voice note attached to the triggering message using
|
|
582
|
+
* Telegram's built-in transcription. Resolves to
|
|
583
|
+
* `{ text, transcriptionId, pending, trialRemainsNum, trialRemainsUntilDate }`.
|
|
584
|
+
* If `pending` is true, the final text arrives later via an
|
|
585
|
+
* `UpdateTranscribedAudio` update rather than this call.
|
|
586
|
+
*/
|
|
587
|
+
async transcribeVoice(id = this.message.id) {
|
|
588
|
+
const peer = await this.peer();
|
|
589
|
+
return this.client.invoke(
|
|
590
|
+
new Api.messages.TranscribeAudio({ peer, msgId: id })
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Translate the triggering message's text into another language (e.g. "en", "id"). */
|
|
595
|
+
async translateText(text, toLang) {
|
|
596
|
+
return this.client.invoke(
|
|
597
|
+
new Api.messages.TranslateText({
|
|
598
|
+
text: [new Api.TextWithEntities({ text, entities: [] })],
|
|
599
|
+
toLang,
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Vote on the poll attached to the triggering message.
|
|
606
|
+
* @param {number|number[]} options - option index (or indexes, for
|
|
607
|
+
* multiple-choice polls), 0-based in the order the poll was created.
|
|
608
|
+
*/
|
|
609
|
+
async votePoll(id = this.message.id, options) {
|
|
610
|
+
const peer = await this.peer();
|
|
611
|
+
const optionBytes = (Array.isArray(options) ? options : [options]).map(
|
|
612
|
+
(i) => Buffer.from([i])
|
|
613
|
+
);
|
|
614
|
+
return this.client.invoke(
|
|
615
|
+
new Api.messages.SendVote({
|
|
616
|
+
peer,
|
|
617
|
+
msgId: id,
|
|
618
|
+
options: optionBytes,
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Retract your vote on the triggering message's poll. */
|
|
624
|
+
async retractVote(id = this.message.id) {
|
|
625
|
+
const peer = await this.peer();
|
|
626
|
+
return this.client.invoke(
|
|
627
|
+
new Api.messages.SendVote({ peer, msgId: id, options: [] })
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ---------------------------------------------------------------------
|
|
632
|
+
// Contacts / history
|
|
633
|
+
// ---------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
/** Block the sender of the triggering message. */
|
|
636
|
+
async blockUser(userId) {
|
|
637
|
+
const senderPeer = userId || await this.message.getInputSender();
|
|
638
|
+
return this.client.invoke(new Api.contacts.Block({ id: senderPeer }));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Unblock the sender of the triggering message. */
|
|
642
|
+
async unblockUser(userId) {
|
|
643
|
+
const senderPeer = userId || await this.message.getInputSender();
|
|
644
|
+
return this.client.invoke(new Api.contacts.Unblock({ id: senderPeer }));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Fetch the most recent messages in the current chat. */
|
|
648
|
+
async getHistory(userId, limit = 20, opts = {}) {
|
|
649
|
+
const peer = userId || await this.peer();
|
|
650
|
+
return this.client.getMessages(peer, { limit, ...opts });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Search for messages containing `query` in the current chat. */
|
|
654
|
+
async searchMessages(query, opts = {}) {
|
|
655
|
+
const peer = await this.peer();
|
|
656
|
+
return this.client.getMessages(peer, { search: query, ...opts });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Unpin every pinned message in the current chat. */
|
|
660
|
+
async unpinAllMessages(userId) {
|
|
661
|
+
const peer = userId || await this.peer();
|
|
662
|
+
return this.client.unpinMessage(peer);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** List participants/members of the current chat (groups/channels only). */
|
|
666
|
+
async getParticipants(opts = {}) {
|
|
667
|
+
const peer = await this.peer();
|
|
668
|
+
return this.client.getParticipants(peer, opts);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Get metadata for a user/chat/channel by @username.
|
|
673
|
+
* Resolves both the basic entity (id, name, username, etc.) and the
|
|
674
|
+
* "full" info (bio for users; description/member count for chats/channels).
|
|
675
|
+
*
|
|
676
|
+
* @param {string} username - with or without the leading @
|
|
677
|
+
* @returns {Promise<{ entity: object, full: object|null }>}
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* const { entity, full } = await ctx.getEntityByUsername("durov");
|
|
681
|
+
* console.log(entity.firstName, full.fullUser.about);
|
|
682
|
+
*/
|
|
683
|
+
async getEntityByUsername(username) {
|
|
684
|
+
const clean = username.replace(/^@/, "");
|
|
685
|
+
const entity = await this.client.getEntity(clean);
|
|
686
|
+
|
|
687
|
+
let full = null;
|
|
688
|
+
try {
|
|
689
|
+
if (entity.className === "User") {
|
|
690
|
+
full = await this.client.invoke(new Api.users.GetFullUser({ id: entity }));
|
|
691
|
+
} else if (entity.className === "Channel") {
|
|
692
|
+
full = await this.client.invoke(
|
|
693
|
+
new Api.channels.GetFullChannel({ channel: entity })
|
|
694
|
+
);
|
|
695
|
+
} else if (entity.className === "Chat") {
|
|
696
|
+
full = await this.client.invoke(
|
|
697
|
+
new Api.messages.GetFullChat({ chatId: entity.id })
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
} catch (err) {
|
|
701
|
+
// full info isn't always available (e.g. restricted/deleted accounts);
|
|
702
|
+
// the basic `entity` above is still returned either way
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return { entity, full };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ---------------------------------------------------------------------
|
|
709
|
+
// Channel / group management (supergroups & channels — see notes below)
|
|
710
|
+
// ---------------------------------------------------------------------
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Ban a user from the current supergroup/channel (they can't rejoin
|
|
714
|
+
* unless unbanned). For basic (non-super) groups, Telegram doesn't
|
|
715
|
+
* support per-user restrictions — use kickUser() instead.
|
|
716
|
+
* @param {string|number} userId
|
|
717
|
+
* @param {object} [opts] e.g. { untilDate: unixTimestamp } for a timed ban
|
|
718
|
+
*/
|
|
719
|
+
async banUser(userId, opts = {}) {
|
|
720
|
+
const peer = await this.peer();
|
|
721
|
+
const participant = await this.client.getInputEntity(userId);
|
|
722
|
+
return this.client.invoke(
|
|
723
|
+
new Api.channels.EditBanned({
|
|
724
|
+
channel: peer,
|
|
725
|
+
participant,
|
|
726
|
+
bannedRights: new Api.ChatBannedRights({
|
|
727
|
+
untilDate: opts.untilDate || 0,
|
|
728
|
+
viewMessages: true,
|
|
729
|
+
sendMessages: true,
|
|
730
|
+
sendMedia: true,
|
|
731
|
+
sendStickers: true,
|
|
732
|
+
sendGifs: true,
|
|
733
|
+
sendGames: true,
|
|
734
|
+
sendInline: true,
|
|
735
|
+
embedLinks: true,
|
|
736
|
+
sendPolls: true,
|
|
737
|
+
changeInfo: true,
|
|
738
|
+
inviteUsers: true,
|
|
739
|
+
pinMessages: true,
|
|
740
|
+
}),
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Clear all restrictions on a user in the current supergroup/channel. */
|
|
746
|
+
async unbanUser(userId) {
|
|
747
|
+
const peer = await this.peer();
|
|
748
|
+
const participant = await this.client.getInputEntity(userId);
|
|
749
|
+
return this.client.invoke(
|
|
750
|
+
new Api.channels.EditBanned({
|
|
751
|
+
channel: peer,
|
|
752
|
+
participant,
|
|
753
|
+
bannedRights: new Api.ChatBannedRights({ untilDate: 0 }),
|
|
754
|
+
})
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Kick a user (remove them, but they CAN rejoin) from a supergroup/channel.
|
|
760
|
+
* For basic groups, this uses messages.DeleteChatUser instead.
|
|
761
|
+
*/
|
|
762
|
+
async kickUser(userId) {
|
|
763
|
+
if (this.isChannel) {
|
|
764
|
+
await this.banUser(userId);
|
|
765
|
+
return this.unbanUser(userId);
|
|
766
|
+
}
|
|
767
|
+
const participant = await this.client.getInputEntity(userId);
|
|
768
|
+
return this.client.invoke(
|
|
769
|
+
new Api.messages.DeleteChatUser({ chatId: this.chatId, userId: participant })
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Promote a user to admin in the current supergroup/channel.
|
|
775
|
+
* `rights` overrides the (fairly permissive) defaults below.
|
|
776
|
+
*/
|
|
777
|
+
async promoteUser(userId, rights = {}, rank = "") {
|
|
778
|
+
const peer = await this.peer();
|
|
779
|
+
const participant = await this.client.getInputEntity(userId);
|
|
780
|
+
return this.client.invoke(
|
|
781
|
+
new Api.channels.EditAdmin({
|
|
782
|
+
channel: peer,
|
|
783
|
+
userId: participant,
|
|
784
|
+
adminRights: new Api.ChatAdminRights({
|
|
785
|
+
changeInfo: true,
|
|
786
|
+
postMessages: true,
|
|
787
|
+
editMessages: true,
|
|
788
|
+
deleteMessages: true,
|
|
789
|
+
banUsers: true,
|
|
790
|
+
inviteUsers: true,
|
|
791
|
+
pinMessages: true,
|
|
792
|
+
addAdmins: false,
|
|
793
|
+
anonymous: false,
|
|
794
|
+
manageCall: true,
|
|
795
|
+
other: true,
|
|
796
|
+
...rights,
|
|
797
|
+
}),
|
|
798
|
+
rank,
|
|
799
|
+
})
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Strip a user of all admin rights in the current supergroup/channel. */
|
|
804
|
+
async demoteUser(userId) {
|
|
805
|
+
return this.promoteUser(
|
|
806
|
+
userId,
|
|
807
|
+
{
|
|
808
|
+
changeInfo: false,
|
|
809
|
+
postMessages: false,
|
|
810
|
+
editMessages: false,
|
|
811
|
+
deleteMessages: false,
|
|
812
|
+
banUsers: false,
|
|
813
|
+
inviteUsers: false,
|
|
814
|
+
pinMessages: false,
|
|
815
|
+
addAdmins: false,
|
|
816
|
+
anonymous: false,
|
|
817
|
+
manageCall: false,
|
|
818
|
+
other: false,
|
|
819
|
+
},
|
|
820
|
+
""
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Invite one or more users to the current supergroup/channel. */
|
|
825
|
+
async inviteToChannel(channelId, userIds) {
|
|
826
|
+
const peer = channelId || await this.peer();
|
|
827
|
+
const users = await Promise.all(
|
|
828
|
+
(Array.isArray(userIds) ? userIds : [userIds]).map((u) =>
|
|
829
|
+
this.client.getInputEntity(u)
|
|
830
|
+
)
|
|
831
|
+
);
|
|
832
|
+
return this.client.invoke(
|
|
833
|
+
new Api.channels.InviteToChannel({ channel: peer, users })
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** Rename the current chat/supergroup/channel. */
|
|
838
|
+
async setChatTitle(id, title) {
|
|
839
|
+
const peer = id || await this.peer();
|
|
840
|
+
if (this.isChannel) {
|
|
841
|
+
return this.client.invoke(new Api.channels.EditTitle({ channel: peer, title }));
|
|
842
|
+
}
|
|
843
|
+
return this.client.invoke(
|
|
844
|
+
new Api.messages.EditChatTitle({ chatId: this.chatId, title })
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Change the current chat/supergroup/channel's profile photo. */
|
|
849
|
+
async setChatPhoto(id, file) {
|
|
850
|
+
const peer = id || await this.peer();
|
|
851
|
+
const uploaded = await this.client.uploadFile({ file, workers: 1 });
|
|
852
|
+
const photo = new Api.InputChatUploadedPhoto({ file: uploaded });
|
|
853
|
+
if (this.isChannel) {
|
|
854
|
+
return this.client.invoke(new Api.channels.EditPhoto({ channel: peer, photo }));
|
|
855
|
+
}
|
|
856
|
+
return this.client.invoke(
|
|
857
|
+
new Api.messages.EditChatPhoto({ chatId: this.chatId, photo })
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ---------------------------------------------------------------------
|
|
862
|
+
// Stickers & GIFs
|
|
863
|
+
// ---------------------------------------------------------------------
|
|
864
|
+
|
|
865
|
+
/** Look up stickers matching an emoji (e.g. "😂"). */
|
|
866
|
+
async searchStickers(emoji) {
|
|
867
|
+
return this.client.invoke(
|
|
868
|
+
new Api.messages.GetStickers({ emoticon: emoji, hash: BigInt(0) })
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Get full info + document list for a sticker set by its short name. */
|
|
873
|
+
async getStickerSet(shortName) {
|
|
874
|
+
return this.client.invoke(
|
|
875
|
+
new Api.messages.GetStickerSet({
|
|
876
|
+
stickerset: new Api.InputStickerSetShortName({ shortName }),
|
|
877
|
+
hash: 0,
|
|
878
|
+
})
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Search Telegram's global GIF results for a query. */
|
|
883
|
+
async searchGifs(query, offset = "") {
|
|
884
|
+
return this.client.invoke(new Api.messages.SearchGifs({ q: query, offset }));
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Save (or unsave) a GIF document to the user's "Saved GIFs".
|
|
889
|
+
* @param {object} document - an Api.Document, e.g. from searchGifs() results
|
|
890
|
+
* @param {boolean} [unsave=false]
|
|
891
|
+
*/
|
|
892
|
+
async saveGif(document, unsave = false) {
|
|
893
|
+
return this.client.invoke(new Api.messages.SaveGif({ id: document, unsave }));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ---------------------------------------------------------------------
|
|
897
|
+
// Privacy & account settings
|
|
898
|
+
// ---------------------------------------------------------------------
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Get your current privacy rules for a given key.
|
|
902
|
+
* @param {"phoneNumber"|"lastSeen"|"chatInvite"|"phoneCall"|"profilePhoto"|"forwards"} [key]
|
|
903
|
+
*/
|
|
904
|
+
async getPrivacySettings(key = "phoneNumber") {
|
|
905
|
+
const keyMap = {
|
|
906
|
+
phoneNumber: () => new Api.InputPrivacyKeyPhoneNumber(),
|
|
907
|
+
lastSeen: () => new Api.InputPrivacyKeyStatusTimestamp(),
|
|
908
|
+
chatInvite: () => new Api.InputPrivacyKeyChatInvite(),
|
|
909
|
+
phoneCall: () => new Api.InputPrivacyKeyPhoneCall(),
|
|
910
|
+
profilePhoto: () => new Api.InputPrivacyKeyProfilePhoto(),
|
|
911
|
+
forwards: () => new Api.InputPrivacyKeyForwards(),
|
|
912
|
+
};
|
|
913
|
+
const build = keyMap[key] || keyMap.phoneNumber;
|
|
914
|
+
return this.client.invoke(new Api.account.GetPrivacy({ key: build() }));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Update your account's first/last name and bio ("about"). */
|
|
918
|
+
async updateProfile(opts = {}) {
|
|
919
|
+
return this.client.invoke(
|
|
920
|
+
new Api.account.UpdateProfile({
|
|
921
|
+
firstName: opts.firstName,
|
|
922
|
+
lastName: opts.lastName,
|
|
923
|
+
about: opts.about,
|
|
924
|
+
})
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Change your account's @username. */
|
|
929
|
+
async updateUsername(username) {
|
|
930
|
+
return this.client.invoke(new Api.account.UpdateUsername({ username }));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** Manually set your account's online/offline status. */
|
|
934
|
+
async setOnlineStatus(online = true) {
|
|
935
|
+
return this.client.invoke(new Api.account.UpdateStatus({ offline: !online }));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// ---------------------------------------------------------------------
|
|
939
|
+
// Advanced file uploads
|
|
940
|
+
// ---------------------------------------------------------------------
|
|
941
|
+
|
|
942
|
+
/** Send a photo/video with the "spoiler" blur overlay. */
|
|
943
|
+
async replyWithSpoiler(file, opts = {}) {
|
|
944
|
+
const peer = await this.peer();
|
|
945
|
+
return this.client.sendFile(peer, { file, spoiler: true, ...opts });
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Send a file with a custom thumbnail image. */
|
|
949
|
+
async replyWithThumb(file, thumb, opts = {}) {
|
|
950
|
+
const peer = await this.peer();
|
|
951
|
+
return this.client.sendFile(peer, { file, thumb, ...opts });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Upload a file with progress tracking, without sending it yet. Returns
|
|
956
|
+
* an uploaded-file handle you can pass as `file` to any replyWith*
|
|
957
|
+
* method — useful for showing upload progress on large files.
|
|
958
|
+
* @param {*} file
|
|
959
|
+
* @param {(uploaded: number, total: number) => void} onProgress
|
|
960
|
+
*/
|
|
961
|
+
async uploadWithProgress(file, onProgress) {
|
|
962
|
+
return this.client.uploadFile({
|
|
963
|
+
file,
|
|
964
|
+
workers: 1,
|
|
965
|
+
progressCallback: onProgress,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
// ---------------------------------------------------------------------
|
|
969
|
+
// Channel / Group Actions
|
|
970
|
+
// ---------------------------------------------------------------------
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Join channel/group by username, id, or invite link
|
|
974
|
+
* @param {string | number} channel - "@username", id, or entity
|
|
975
|
+
* @example ctx.joinChannel("XazepysK")
|
|
976
|
+
*/
|
|
977
|
+
async joinChannel(id) {
|
|
978
|
+
const entity = await this.client.getInputEntity(id);
|
|
979
|
+
return this.client.invoke(new Api.channels.JoinChannel({ channel: entity }));
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Join private group/channel via invite hash
|
|
984
|
+
* @param {string} hash - hash from t.me/+HASH
|
|
985
|
+
* @example ctx.joinByInvite("AAAAAE2x...")
|
|
986
|
+
*/
|
|
987
|
+
async joinByInvite(hash) {
|
|
988
|
+
return this.client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Leave channel/group
|
|
993
|
+
* @param {string | number} channel - "@username", id, or entity. Default: chatId
|
|
994
|
+
*/
|
|
995
|
+
async leaveChannel(id = null) {
|
|
996
|
+
const peer = id? await this.client.getInputEntity(id) : await this.peer();
|
|
997
|
+
return this.client.invoke(new Api.channels.LeaveChannel({ channel: peer }));
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Get buffer + metadata from media with messageId
|
|
1002
|
+
* @param {number} [messageId] - messageId
|
|
1003
|
+
* @param {object} [opts={}] - opts downloadMedia
|
|
1004
|
+
* @returns {Promise<{buffer: Buffer, metadata: object, filename: string}>}
|
|
1005
|
+
*/
|
|
1006
|
+
async saveMediaMessage(messageId = null, opts = {}) {
|
|
1007
|
+
const peer = await this.peer();
|
|
1008
|
+
const targetMsg = messageId ? (await this.client.getMessages(peer, { ids: [messageId] }))[0] : this.message;
|
|
1009
|
+
|
|
1010
|
+
if (!targetMsg) throw new Error(`${messageId} not found`);
|
|
1011
|
+
if (!targetMsg.media) throw new Error("message doesnt have media");
|
|
1012
|
+
const buffer = await this.client.downloadMedia(targetMsg, opts);
|
|
1013
|
+
const media = targetMsg.media;
|
|
1014
|
+
let metadata = {
|
|
1015
|
+
type: targetMsg.media.className,
|
|
1016
|
+
size: null,
|
|
1017
|
+
mimeType: null,
|
|
1018
|
+
fileName: null,
|
|
1019
|
+
width: null,
|
|
1020
|
+
height: null,
|
|
1021
|
+
duration: null,
|
|
1022
|
+
date: targetMsg.date,
|
|
1023
|
+
chatId: targetMsg.chatId,
|
|
1024
|
+
messageId: targetMsg.id,
|
|
1025
|
+
senderId: targetMsg.senderId,
|
|
1026
|
+
};
|
|
1027
|
+
if (media.className === "MessageMediaPhoto") {
|
|
1028
|
+
const biggest = media.photo.sizes[media.photo.sizes.length - 1];
|
|
1029
|
+
metadata.size = biggest?.size || null;
|
|
1030
|
+
metadata.width = biggest?.w || null;
|
|
1031
|
+
metadata.height = biggest?.h || null;
|
|
1032
|
+
metadata.fileName = `photo_${targetMsg.id}.jpg`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (media.className === "MessageMediaDocument") {
|
|
1036
|
+
const doc = media.document;
|
|
1037
|
+
metadata.size = doc.size;
|
|
1038
|
+
metadata.mimeType = doc.mimeType;
|
|
1039
|
+
metadata.date = doc.date;
|
|
1040
|
+
const attr = doc.attributes.find(a => a.className === "DocumentAttributeFilename");
|
|
1041
|
+
metadata.fileName = attr?.fileName || `file_${targetMsg.id}`;
|
|
1042
|
+
const videoAttr = doc.attributes.find(a => a.className === "DocumentAttributeVideo");
|
|
1043
|
+
if (videoAttr) {
|
|
1044
|
+
metadata.width = videoAttr.w;
|
|
1045
|
+
metadata.height = videoAttr.h;
|
|
1046
|
+
metadata.duration = videoAttr.duration;
|
|
1047
|
+
}
|
|
1048
|
+
const audioAttr = doc.attributes.find(a => a.className === "DocumentAttributeAudio");
|
|
1049
|
+
if (audioAttr) metadata.duration = audioAttr.duration;
|
|
1050
|
+
}
|
|
1051
|
+
return { buffer, metadata, filename: metadata.fileName };
|
|
1052
|
+
}
|
|
340
1053
|
}
|
|
341
1054
|
|
|
342
1055
|
module.exports = { Context, buildButtons };
|