xzcgram 0.0.1 → 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 +209 -16
- package/src/client.js +32 -8
- package/src/context.js +823 -63
package/src/context.js
CHANGED
|
@@ -33,12 +33,83 @@ class Context {
|
|
|
33
33
|
this.event = event;
|
|
34
34
|
/** Raw GramJS message object */
|
|
35
35
|
this.message = event.message;
|
|
36
|
-
/** Chat/peer id where the message was sent */
|
|
36
|
+
/** Chat/peer id where the message was sent (numeric, informational) */
|
|
37
37
|
this.chatId = this.message.chatId;
|
|
38
38
|
/** Full text of the incoming message */
|
|
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;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolves the correct peer to send to/act on.
|
|
99
|
+
* Uses message.getInputChat() first, since it carries the access_hash
|
|
100
|
+
* from the incoming update — this avoids "Could not find the input
|
|
101
|
+
* entity" errors that happen when using a raw numeric chatId for a
|
|
102
|
+
* user/chat the client hasn't cached yet (e.g. first DM from someone new).
|
|
103
|
+
* Falls back to the raw chatId if getInputChat() can't resolve anything.
|
|
104
|
+
*/
|
|
105
|
+
async peer() {
|
|
106
|
+
try {
|
|
107
|
+
const inputChat = await this.message.getInputChat();
|
|
108
|
+
if (inputChat) return inputChat;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// fall through to raw chatId below
|
|
111
|
+
}
|
|
112
|
+
return this.chatId;
|
|
42
113
|
}
|
|
43
114
|
|
|
44
115
|
// ---------------------------------------------------------------------
|
|
@@ -46,13 +117,15 @@ class Context {
|
|
|
46
117
|
// ---------------------------------------------------------------------
|
|
47
118
|
|
|
48
119
|
/** Reply in the same chat the message came from. */
|
|
49
|
-
reply(text, opts = {}) {
|
|
50
|
-
|
|
120
|
+
async reply(text, opts = {}) {
|
|
121
|
+
const peer = await this.peer();
|
|
122
|
+
return this.client.sendMessage(peer, { message: text, ...opts });
|
|
51
123
|
}
|
|
52
124
|
|
|
53
125
|
/** Reply directly to the triggering message (quote-style reply). */
|
|
54
|
-
replyQuote(text, opts = {}) {
|
|
55
|
-
|
|
126
|
+
async replyQuote(text, opts = {}) {
|
|
127
|
+
const peer = await this.peer();
|
|
128
|
+
return this.client.sendMessage(peer, {
|
|
56
129
|
message: text,
|
|
57
130
|
replyTo: this.message.id,
|
|
58
131
|
...opts,
|
|
@@ -60,8 +133,9 @@ class Context {
|
|
|
60
133
|
}
|
|
61
134
|
|
|
62
135
|
/** Edit a message previously sent in this chat (must be your own message). */
|
|
63
|
-
editMessage(messageId, text, opts = {}) {
|
|
64
|
-
|
|
136
|
+
async editMessage(messageId, text, opts = {}) {
|
|
137
|
+
const peer = await this.peer();
|
|
138
|
+
return this.client.editMessage(peer, {
|
|
65
139
|
message: messageId,
|
|
66
140
|
text,
|
|
67
141
|
...opts,
|
|
@@ -73,13 +147,15 @@ class Context {
|
|
|
73
147
|
// ---------------------------------------------------------------------
|
|
74
148
|
|
|
75
149
|
/** Send a photo. `file` can be a path, Buffer, URL, or existing file id. */
|
|
76
|
-
replyWithPhoto(file, opts = {}) {
|
|
77
|
-
|
|
150
|
+
async replyWithPhoto(file, opts = {}) {
|
|
151
|
+
const peer = await this.peer();
|
|
152
|
+
return this.client.sendFile(peer, { file, ...opts });
|
|
78
153
|
}
|
|
79
154
|
|
|
80
155
|
/** Send a video. Pass `opts.supportsStreaming = true` for streamable playback. */
|
|
81
|
-
replyWithVideo(file, opts = {}) {
|
|
82
|
-
|
|
156
|
+
async replyWithVideo(file, opts = {}) {
|
|
157
|
+
const peer = await this.peer();
|
|
158
|
+
return this.client.sendFile(peer, {
|
|
83
159
|
file,
|
|
84
160
|
supportsStreaming: true,
|
|
85
161
|
...opts,
|
|
@@ -87,23 +163,27 @@ class Context {
|
|
|
87
163
|
}
|
|
88
164
|
|
|
89
165
|
/** Send a round "video note" (the circular video bubble). */
|
|
90
|
-
replyWithVideoNote(file, opts = {}) {
|
|
91
|
-
|
|
166
|
+
async replyWithVideoNote(file, opts = {}) {
|
|
167
|
+
const peer = await this.peer();
|
|
168
|
+
return this.client.sendFile(peer, { file, videoNote: true, ...opts });
|
|
92
169
|
}
|
|
93
170
|
|
|
94
171
|
/** Send an audio file (music, shown with player + duration/title). */
|
|
95
|
-
replyWithAudio(file, opts = {}) {
|
|
96
|
-
|
|
172
|
+
async replyWithAudio(file, opts = {}) {
|
|
173
|
+
const peer = await this.peer();
|
|
174
|
+
return this.client.sendFile(peer, { file, ...opts });
|
|
97
175
|
}
|
|
98
176
|
|
|
99
177
|
/** Send a voice note (the waveform bubble). */
|
|
100
|
-
replyWithVoice(file, opts = {}) {
|
|
101
|
-
|
|
178
|
+
async replyWithVoice(file, opts = {}) {
|
|
179
|
+
const peer = await this.peer();
|
|
180
|
+
return this.client.sendFile(peer, { file, voiceNote: true, ...opts });
|
|
102
181
|
}
|
|
103
182
|
|
|
104
183
|
/** Send any file as a generic document. */
|
|
105
|
-
replyWithDocument(file, opts = {}) {
|
|
106
|
-
|
|
184
|
+
async replyWithDocument(file, opts = {}) {
|
|
185
|
+
const peer = await this.peer();
|
|
186
|
+
return this.client.sendFile(peer, {
|
|
107
187
|
file,
|
|
108
188
|
forceDocument: true,
|
|
109
189
|
...opts,
|
|
@@ -111,13 +191,15 @@ class Context {
|
|
|
111
191
|
}
|
|
112
192
|
|
|
113
193
|
/** Send a sticker (.webp/.tgs file or existing file reference). */
|
|
114
|
-
replyWithSticker(file, opts = {}) {
|
|
115
|
-
|
|
194
|
+
async replyWithSticker(file, opts = {}) {
|
|
195
|
+
const peer = await this.peer();
|
|
196
|
+
return this.client.sendFile(peer, { file, ...opts });
|
|
116
197
|
}
|
|
117
198
|
|
|
118
199
|
/** Send an animated GIF. */
|
|
119
|
-
replyWithAnimation(file, opts = {}) {
|
|
120
|
-
|
|
200
|
+
async replyWithAnimation(file, opts = {}) {
|
|
201
|
+
const peer = await this.peer();
|
|
202
|
+
return this.client.sendFile(peer, {
|
|
121
203
|
file,
|
|
122
204
|
attributes: [new Api.DocumentAttributeAnimated()],
|
|
123
205
|
...opts,
|
|
@@ -125,8 +207,9 @@ class Context {
|
|
|
125
207
|
}
|
|
126
208
|
|
|
127
209
|
/** Send multiple files as an album/media group. `files` is an array. */
|
|
128
|
-
replyWithMediaGroup(files, opts = {}) {
|
|
129
|
-
|
|
210
|
+
async replyWithMediaGroup(files, opts = {}) {
|
|
211
|
+
const peer = await this.peer();
|
|
212
|
+
return this.client.sendFile(peer, { file: files, ...opts });
|
|
130
213
|
}
|
|
131
214
|
|
|
132
215
|
// ---------------------------------------------------------------------
|
|
@@ -139,8 +222,9 @@ class Context {
|
|
|
139
222
|
* @param {Array<Array<{text: string, data?: string, url?: string}>>} rows
|
|
140
223
|
* @param {object} [opts]
|
|
141
224
|
*/
|
|
142
|
-
replyWithButtons(text, rows, opts = {}) {
|
|
143
|
-
|
|
225
|
+
async replyWithButtons(text, rows, opts = {}) {
|
|
226
|
+
const peer = await this.peer();
|
|
227
|
+
return this.client.sendMessage(peer, {
|
|
144
228
|
message: text,
|
|
145
229
|
buttons: buildButtons(rows, true),
|
|
146
230
|
...opts,
|
|
@@ -153,8 +237,9 @@ class Context {
|
|
|
153
237
|
* @param {Array<Array<{text: string}>>} rows
|
|
154
238
|
* @param {object} [opts]
|
|
155
239
|
*/
|
|
156
|
-
replyWithKeyboard(text, rows, opts = {}) {
|
|
157
|
-
|
|
240
|
+
async replyWithKeyboard(text, rows, opts = {}) {
|
|
241
|
+
const peer = await this.peer();
|
|
242
|
+
return this.client.sendMessage(peer, {
|
|
158
243
|
message: text,
|
|
159
244
|
buttons: buildButtons(rows, false),
|
|
160
245
|
...opts,
|
|
@@ -167,19 +252,33 @@ class Context {
|
|
|
167
252
|
* @param {string[]} answers
|
|
168
253
|
* @param {object} [opts] e.g. { multipleChoice: true, quiz: false }
|
|
169
254
|
*/
|
|
170
|
-
replyWithPoll(question, answers, opts = {}) {
|
|
255
|
+
async replyWithPoll(question, answers, opts = {}) {
|
|
256
|
+
const {
|
|
257
|
+
publicVoters = true,
|
|
258
|
+
multipleChoice = true,
|
|
259
|
+
quiz = false
|
|
260
|
+
} = opts;
|
|
261
|
+
const peer = await this.peer();
|
|
171
262
|
return this.client.invoke(
|
|
172
263
|
new Api.messages.SendMedia({
|
|
173
|
-
peer
|
|
264
|
+
peer,
|
|
174
265
|
media: new Api.InputMediaPoll({
|
|
175
266
|
poll: new Api.Poll({
|
|
176
267
|
id: BigInt(Date.now()),
|
|
177
|
-
question
|
|
268
|
+
question: new Api.TextWithEntities({
|
|
269
|
+
text: question,
|
|
270
|
+
entities: [],
|
|
271
|
+
}),
|
|
178
272
|
answers: answers.map(
|
|
179
273
|
(text, i) =>
|
|
180
|
-
new Api.PollAnswer({
|
|
274
|
+
new Api.PollAnswer({
|
|
275
|
+
text: new Api.TextWithEntities({ text, entities: [] }),
|
|
276
|
+
option: Buffer.from([i]),
|
|
277
|
+
})
|
|
181
278
|
),
|
|
182
|
-
multipleChoice: !!
|
|
279
|
+
multipleChoice: !!multipleChoice,
|
|
280
|
+
publicVoters,
|
|
281
|
+
...opts
|
|
183
282
|
}),
|
|
184
283
|
}),
|
|
185
284
|
message: "",
|
|
@@ -189,25 +288,27 @@ class Context {
|
|
|
189
288
|
}
|
|
190
289
|
|
|
191
290
|
/** Send a geographic location. */
|
|
192
|
-
replyWithLocation(latitude, longitude, opts = {}) {
|
|
291
|
+
async replyWithLocation(latitude, longitude, opts = {}) {
|
|
292
|
+
const peer = await this.peer();
|
|
193
293
|
return this.client.invoke(
|
|
194
294
|
new Api.messages.SendMedia({
|
|
195
|
-
peer
|
|
295
|
+
peer,
|
|
196
296
|
media: new Api.InputMediaGeoPoint({
|
|
197
297
|
geoPoint: new Api.InputGeoPoint({ lat: latitude, long: longitude }),
|
|
198
298
|
}),
|
|
199
299
|
message: "",
|
|
200
|
-
randomId: BigInt(
|
|
300
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
201
301
|
...opts,
|
|
202
302
|
})
|
|
203
303
|
);
|
|
204
304
|
}
|
|
205
|
-
|
|
305
|
+
|
|
206
306
|
/** Send a contact card. */
|
|
207
|
-
replyWithContact(phoneNumber, firstName, lastName = "", opts = {}) {
|
|
307
|
+
async replyWithContact(phoneNumber, firstName, lastName = "", opts = {}) {
|
|
308
|
+
const peer = await this.peer();
|
|
208
309
|
return this.client.invoke(
|
|
209
310
|
new Api.messages.SendMedia({
|
|
210
|
-
peer
|
|
311
|
+
peer,
|
|
211
312
|
media: new Api.InputMediaContact({
|
|
212
313
|
phoneNumber,
|
|
213
314
|
firstName,
|
|
@@ -215,56 +316,58 @@ class Context {
|
|
|
215
316
|
vcard: "",
|
|
216
317
|
}),
|
|
217
318
|
message: "",
|
|
218
|
-
randomId: BigInt(
|
|
319
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
219
320
|
...opts,
|
|
220
321
|
})
|
|
221
322
|
);
|
|
222
323
|
}
|
|
223
324
|
|
|
224
325
|
/** Send an animated dice/emoji reaction (🎲 🎯 🏀 ⚽ 🎰 🎳). */
|
|
225
|
-
replyWithDice(emoji = "🎰", opts = {}) {
|
|
326
|
+
async replyWithDice(emoji = "🎰", opts = {}) {
|
|
327
|
+
const peer = await this.peer();
|
|
226
328
|
return this.client.invoke(
|
|
227
329
|
new Api.messages.SendMedia({
|
|
228
|
-
peer
|
|
330
|
+
peer,
|
|
229
331
|
media: new Api.InputMediaDice({ emoticon: emoji }),
|
|
230
332
|
message: "",
|
|
231
|
-
randomId: BigInt(
|
|
333
|
+
randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
|
232
334
|
...opts,
|
|
233
335
|
})
|
|
234
336
|
);
|
|
235
337
|
}
|
|
236
338
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
/** Delete the triggering message (revokes for everyone by default). */
|
|
242
|
-
deleteMessage() {
|
|
243
|
-
return this.client.deleteMessages(this.chatId, [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], {
|
|
244
343
|
revoke: true,
|
|
245
344
|
});
|
|
246
345
|
}
|
|
247
346
|
|
|
248
|
-
/** Forward the triggering message to another chat. */
|
|
249
|
-
forwardMessage(toChatId) {
|
|
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();
|
|
250
350
|
return this.client.forwardMessages(toChatId, {
|
|
251
|
-
messages: [
|
|
252
|
-
fromPeer:
|
|
351
|
+
messages: [id],
|
|
352
|
+
fromPeer: peer,
|
|
253
353
|
});
|
|
254
354
|
}
|
|
255
355
|
|
|
256
|
-
/** Pin the triggering message in the current chat. */
|
|
257
|
-
pinMessage(opts = {}) {
|
|
258
|
-
|
|
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);
|
|
259
360
|
}
|
|
260
361
|
|
|
261
|
-
/** Unpin the triggering message in the current chat. */
|
|
262
|
-
unpinMessage() {
|
|
263
|
-
|
|
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);
|
|
264
366
|
}
|
|
265
367
|
|
|
266
368
|
/** Show the "typing..." / "sending photo..." indicator. */
|
|
267
|
-
sendChatAction(action = "typing") {
|
|
369
|
+
async sendChatAction(action = "typing") {
|
|
370
|
+
const peer = await this.peer();
|
|
268
371
|
const actions = {
|
|
269
372
|
typing: new Api.SendMessageTypingAction(),
|
|
270
373
|
photo: new Api.SendMessageUploadPhotoAction({ progress: 0 }),
|
|
@@ -275,7 +378,7 @@ class Context {
|
|
|
275
378
|
};
|
|
276
379
|
return this.client.invoke(
|
|
277
380
|
new Api.messages.SetTyping({
|
|
278
|
-
peer
|
|
381
|
+
peer,
|
|
279
382
|
action: actions[action] || actions.typing,
|
|
280
383
|
})
|
|
281
384
|
);
|
|
@@ -290,6 +393,663 @@ class Context {
|
|
|
290
393
|
getChat() {
|
|
291
394
|
return this.message.getChat();
|
|
292
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
|
+
}
|
|
293
1053
|
}
|
|
294
1054
|
|
|
295
1055
|
module.exports = { Context, buildButtons };
|