xzcgram 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/bot.js +59 -18
  3. package/src/context.js +96 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xzcgram",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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",
package/src/bot.js CHANGED
@@ -1,9 +1,26 @@
1
1
  const { NewMessage } = require("telegram/events");
2
- const { NewCallbackQuery } = require("telegram/events/NewCallbackQuery");
3
2
  const { Context } = require("./context");
4
3
 
4
+ // CallbackQuery (inline button presses) — imported defensively since its
5
+ // export path has moved between "telegram" package versions. If it can't
6
+ // be found, bot.action() handlers simply won't fire, but everything else
7
+ // (commands, hears, media, etc.) keeps working.
8
+ let CallbackQuery = null;
9
+ try {
10
+ ({ CallbackQuery } = require("telegram/events"));
11
+ } catch (err) {
12
+ /* ignore, try next path */
13
+ }
14
+ if (!CallbackQuery) {
15
+ try {
16
+ ({ CallbackQuery } = require("telegram/events/CallbackQuery"));
17
+ } catch (err) {
18
+ /* ignore, handled below */
19
+ }
20
+ }
21
+
5
22
  /**
6
- * CallbackContext wraps a GramJS NewCallbackQuery event, exposed to
23
+ * CallbackContext wraps a GramJS CallbackQuery event, exposed to
7
24
  * handlers registered via bot.action(...).
8
25
  */
9
26
  class CallbackContext {
@@ -17,6 +34,20 @@ class CallbackContext {
17
34
  this.data = event.data ? event.data.toString("utf-8") : "";
18
35
  }
19
36
 
37
+ /**
38
+ * Resolves the correct peer to send to/act on. See Context._peer() for
39
+ * why this is needed instead of a raw numeric chatId.
40
+ */
41
+ async _peer() {
42
+ try {
43
+ const inputChat = await this.event.getInputChat();
44
+ if (inputChat) return inputChat;
45
+ } catch (err) {
46
+ // fall through to raw chatId below
47
+ }
48
+ return this.chatId;
49
+ }
50
+
20
51
  /** Answer the callback query (e.g. show a small toast/alert to the user). */
21
52
  answer(text = "", opts = {}) {
22
53
  return this.event.answer({ message: text, ...opts });
@@ -28,8 +59,9 @@ class CallbackContext {
28
59
  }
29
60
 
30
61
  /** Send a new message in the same chat as the callback query. */
31
- reply(text, opts = {}) {
32
- return this.client.sendMessage(this.chatId, { message: text, ...opts });
62
+ async reply(text, opts = {}) {
63
+ const peer = await this._peer();
64
+ return this.client.sendMessage(peer, { message: text, ...opts });
33
65
  }
34
66
  }
35
67
 
@@ -135,21 +167,30 @@ class Bot {
135
167
  }
136
168
  }, new NewMessage({}));
137
169
 
138
- this.client.addEventHandler(async (event) => {
139
- try {
140
- const ctx = new CallbackContext(this.client, event);
141
-
142
- for (const h of this.handlers.action) {
143
- const matched =
144
- h.pattern instanceof RegExp
145
- ? h.pattern.test(ctx.data)
146
- : ctx.data === h.pattern;
147
- if (matched) return await h.fn(ctx);
170
+ if (CallbackQuery) {
171
+ this.client.addEventHandler(async (event) => {
172
+ try {
173
+ const ctx = new CallbackContext(this.client, event);
174
+
175
+ for (const h of this.handlers.action) {
176
+ const matched =
177
+ h.pattern instanceof RegExp
178
+ ? h.pattern.test(ctx.data)
179
+ : ctx.data === h.pattern;
180
+ if (matched) return await h.fn(ctx);
181
+ }
182
+ } catch (err) {
183
+ console.error("[xzcgram] action handler error:", err);
148
184
  }
149
- } catch (err) {
150
- console.error("[xzcgram] action handler error:", err);
151
- }
152
- }, new NewCallbackQuery({}));
185
+ }, new CallbackQuery({}));
186
+ } else if (this.handlers.action.length > 0) {
187
+ console.warn(
188
+ "[xzcgram] bot.action() handlers were registered, but this version " +
189
+ "of the 'telegram' package doesn't expose a CallbackQuery event " +
190
+ "class — inline button presses won't be handled. Try updating " +
191
+ "the 'telegram' package."
192
+ );
193
+ }
153
194
  }
154
195
  }
155
196
 
package/src/context.js CHANGED
@@ -33,7 +33,7 @@ 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 || "";
@@ -41,18 +41,38 @@ class Context {
41
41
  this.args = this.text.split(" ").slice(1);
42
42
  }
43
43
 
44
+ /**
45
+ * Resolves the correct peer to send to/act on.
46
+ * Uses message.getInputChat() first, since it carries the access_hash
47
+ * from the incoming update — this avoids "Could not find the input
48
+ * entity" errors that happen when using a raw numeric chatId for a
49
+ * user/chat the client hasn't cached yet (e.g. first DM from someone new).
50
+ * Falls back to the raw chatId if getInputChat() can't resolve anything.
51
+ */
52
+ async _peer() {
53
+ try {
54
+ const inputChat = await this.message.getInputChat();
55
+ if (inputChat) return inputChat;
56
+ } catch (err) {
57
+ // fall through to raw chatId below
58
+ }
59
+ return this.chatId;
60
+ }
61
+
44
62
  // ---------------------------------------------------------------------
45
63
  // Text
46
64
  // ---------------------------------------------------------------------
47
65
 
48
66
  /** Reply in the same chat the message came from. */
49
- reply(text, opts = {}) {
50
- return this.client.sendMessage(this.chatId, { message: text, ...opts });
67
+ async reply(text, opts = {}) {
68
+ const peer = await this._peer();
69
+ return this.client.sendMessage(peer, { message: text, ...opts });
51
70
  }
52
71
 
53
72
  /** Reply directly to the triggering message (quote-style reply). */
54
- replyQuote(text, opts = {}) {
55
- return this.client.sendMessage(this.chatId, {
73
+ async replyQuote(text, opts = {}) {
74
+ const peer = await this._peer();
75
+ return this.client.sendMessage(peer, {
56
76
  message: text,
57
77
  replyTo: this.message.id,
58
78
  ...opts,
@@ -60,8 +80,9 @@ class Context {
60
80
  }
61
81
 
62
82
  /** Edit a message previously sent in this chat (must be your own message). */
63
- editMessage(messageId, text, opts = {}) {
64
- return this.client.editMessage(this.chatId, {
83
+ async editMessage(messageId, text, opts = {}) {
84
+ const peer = await this._peer();
85
+ return this.client.editMessage(peer, {
65
86
  message: messageId,
66
87
  text,
67
88
  ...opts,
@@ -73,13 +94,15 @@ class Context {
73
94
  // ---------------------------------------------------------------------
74
95
 
75
96
  /** Send a photo. `file` can be a path, Buffer, URL, or existing file id. */
76
- replyWithPhoto(file, opts = {}) {
77
- return this.client.sendFile(this.chatId, { file, ...opts });
97
+ async replyWithPhoto(file, opts = {}) {
98
+ const peer = await this._peer();
99
+ return this.client.sendFile(peer, { file, ...opts });
78
100
  }
79
101
 
80
102
  /** Send a video. Pass `opts.supportsStreaming = true` for streamable playback. */
81
- replyWithVideo(file, opts = {}) {
82
- return this.client.sendFile(this.chatId, {
103
+ async replyWithVideo(file, opts = {}) {
104
+ const peer = await this._peer();
105
+ return this.client.sendFile(peer, {
83
106
  file,
84
107
  supportsStreaming: true,
85
108
  ...opts,
@@ -87,23 +110,27 @@ class Context {
87
110
  }
88
111
 
89
112
  /** Send a round "video note" (the circular video bubble). */
90
- replyWithVideoNote(file, opts = {}) {
91
- return this.client.sendFile(this.chatId, { file, videoNote: true, ...opts });
113
+ async replyWithVideoNote(file, opts = {}) {
114
+ const peer = await this._peer();
115
+ return this.client.sendFile(peer, { file, videoNote: true, ...opts });
92
116
  }
93
117
 
94
118
  /** Send an audio file (music, shown with player + duration/title). */
95
- replyWithAudio(file, opts = {}) {
96
- return this.client.sendFile(this.chatId, { file, ...opts });
119
+ async replyWithAudio(file, opts = {}) {
120
+ const peer = await this._peer();
121
+ return this.client.sendFile(peer, { file, ...opts });
97
122
  }
98
123
 
99
124
  /** Send a voice note (the waveform bubble). */
100
- replyWithVoice(file, opts = {}) {
101
- return this.client.sendFile(this.chatId, { file, voiceNote: true, ...opts });
125
+ async replyWithVoice(file, opts = {}) {
126
+ const peer = await this._peer();
127
+ return this.client.sendFile(peer, { file, voiceNote: true, ...opts });
102
128
  }
103
129
 
104
130
  /** Send any file as a generic document. */
105
- replyWithDocument(file, opts = {}) {
106
- return this.client.sendFile(this.chatId, {
131
+ async replyWithDocument(file, opts = {}) {
132
+ const peer = await this._peer();
133
+ return this.client.sendFile(peer, {
107
134
  file,
108
135
  forceDocument: true,
109
136
  ...opts,
@@ -111,13 +138,15 @@ class Context {
111
138
  }
112
139
 
113
140
  /** Send a sticker (.webp/.tgs file or existing file reference). */
114
- replyWithSticker(file, opts = {}) {
115
- return this.client.sendFile(this.chatId, { file, ...opts });
141
+ async replyWithSticker(file, opts = {}) {
142
+ const peer = await this._peer();
143
+ return this.client.sendFile(peer, { file, ...opts });
116
144
  }
117
145
 
118
146
  /** Send an animated GIF. */
119
- replyWithAnimation(file, opts = {}) {
120
- return this.client.sendFile(this.chatId, {
147
+ async replyWithAnimation(file, opts = {}) {
148
+ const peer = await this._peer();
149
+ return this.client.sendFile(peer, {
121
150
  file,
122
151
  attributes: [new Api.DocumentAttributeAnimated()],
123
152
  ...opts,
@@ -125,8 +154,9 @@ class Context {
125
154
  }
126
155
 
127
156
  /** Send multiple files as an album/media group. `files` is an array. */
128
- replyWithMediaGroup(files, opts = {}) {
129
- return this.client.sendFile(this.chatId, { file: files, ...opts });
157
+ async replyWithMediaGroup(files, opts = {}) {
158
+ const peer = await this._peer();
159
+ return this.client.sendFile(peer, { file: files, ...opts });
130
160
  }
131
161
 
132
162
  // ---------------------------------------------------------------------
@@ -139,8 +169,9 @@ class Context {
139
169
  * @param {Array<Array<{text: string, data?: string, url?: string}>>} rows
140
170
  * @param {object} [opts]
141
171
  */
142
- replyWithButtons(text, rows, opts = {}) {
143
- return this.client.sendMessage(this.chatId, {
172
+ async replyWithButtons(text, rows, opts = {}) {
173
+ const peer = await this._peer();
174
+ return this.client.sendMessage(peer, {
144
175
  message: text,
145
176
  buttons: buildButtons(rows, true),
146
177
  ...opts,
@@ -153,8 +184,9 @@ class Context {
153
184
  * @param {Array<Array<{text: string}>>} rows
154
185
  * @param {object} [opts]
155
186
  */
156
- replyWithKeyboard(text, rows, opts = {}) {
157
- return this.client.sendMessage(this.chatId, {
187
+ async replyWithKeyboard(text, rows, opts = {}) {
188
+ const peer = await this._peer();
189
+ return this.client.sendMessage(peer, {
158
190
  message: text,
159
191
  buttons: buildButtons(rows, false),
160
192
  ...opts,
@@ -167,17 +199,24 @@ class Context {
167
199
  * @param {string[]} answers
168
200
  * @param {object} [opts] e.g. { multipleChoice: true, quiz: false }
169
201
  */
170
- replyWithPoll(question, answers, opts = {}) {
202
+ async replyWithPoll(question, answers, opts = {}) {
203
+ const peer = await this._peer();
171
204
  return this.client.invoke(
172
205
  new Api.messages.SendMedia({
173
- peer: this.chatId,
206
+ peer,
174
207
  media: new Api.InputMediaPoll({
175
208
  poll: new Api.Poll({
176
209
  id: BigInt(Date.now()),
177
- question,
210
+ question: new Api.TextWithEntities({
211
+ text: question,
212
+ entities: [],
213
+ }),
178
214
  answers: answers.map(
179
215
  (text, i) =>
180
- new Api.PollAnswer({ text, option: Buffer.from([i]) })
216
+ new Api.PollAnswer({
217
+ text: new Api.TextWithEntities({ text, entities: [] }),
218
+ option: Buffer.from([i]),
219
+ })
181
220
  ),
182
221
  multipleChoice: !!opts.multipleChoice,
183
222
  }),
@@ -189,10 +228,11 @@ class Context {
189
228
  }
190
229
 
191
230
  /** Send a geographic location. */
192
- replyWithLocation(latitude, longitude, opts = {}) {
231
+ async replyWithLocation(latitude, longitude, opts = {}) {
232
+ const peer = await this._peer();
193
233
  return this.client.invoke(
194
234
  new Api.messages.SendMedia({
195
- peer: this.chatId,
235
+ peer,
196
236
  media: new Api.InputMediaGeoPoint({
197
237
  geoPoint: new Api.InputGeoPoint({ lat: latitude, long: longitude }),
198
238
  }),
@@ -204,10 +244,11 @@ class Context {
204
244
  }
205
245
 
206
246
  /** Send a contact card. */
207
- replyWithContact(phoneNumber, firstName, lastName = "", opts = {}) {
247
+ async replyWithContact(phoneNumber, firstName, lastName = "", opts = {}) {
248
+ const peer = await this._peer();
208
249
  return this.client.invoke(
209
250
  new Api.messages.SendMedia({
210
- peer: this.chatId,
251
+ peer,
211
252
  media: new Api.InputMediaContact({
212
253
  phoneNumber,
213
254
  firstName,
@@ -222,10 +263,11 @@ class Context {
222
263
  }
223
264
 
224
265
  /** Send an animated dice/emoji reaction (🎲 🎯 🏀 ⚽ 🎰 🎳). */
225
- replyWithDice(emoji = "🎰", opts = {}) {
266
+ async replyWithDice(emoji = "🎲", opts = {}) {
267
+ const peer = await this._peer();
226
268
  return this.client.invoke(
227
269
  new Api.messages.SendMedia({
228
- peer: this.chatId,
270
+ peer,
229
271
  media: new Api.InputMediaDice({ emoticon: emoji }),
230
272
  message: "",
231
273
  randomId: BigInt(Date.now()),
@@ -239,32 +281,37 @@ class Context {
239
281
  // ---------------------------------------------------------------------
240
282
 
241
283
  /** Delete the triggering message (revokes for everyone by default). */
242
- deleteMessage() {
243
- return this.client.deleteMessages(this.chatId, [this.message.id], {
284
+ async deleteMessage() {
285
+ const peer = await this._peer();
286
+ return this.client.deleteMessages(peer, [this.message.id], {
244
287
  revoke: true,
245
288
  });
246
289
  }
247
290
 
248
291
  /** Forward the triggering message to another chat. */
249
- forwardMessage(toChatId) {
292
+ async forwardMessage(toChatId) {
293
+ const peer = await this._peer();
250
294
  return this.client.forwardMessages(toChatId, {
251
295
  messages: [this.message.id],
252
- fromPeer: this.chatId,
296
+ fromPeer: peer,
253
297
  });
254
298
  }
255
299
 
256
300
  /** Pin the triggering message in the current chat. */
257
- pinMessage(opts = {}) {
258
- return this.client.pinMessage(this.chatId, this.message.id, opts);
301
+ async pinMessage(opts = {}) {
302
+ const peer = await this._peer();
303
+ return this.client.pinMessage(peer, this.message.id, opts);
259
304
  }
260
305
 
261
306
  /** Unpin the triggering message in the current chat. */
262
- unpinMessage() {
263
- return this.client.unpinMessage(this.chatId, this.message.id);
307
+ async unpinMessage() {
308
+ const peer = await this._peer();
309
+ return this.client.unpinMessage(peer, this.message.id);
264
310
  }
265
311
 
266
312
  /** Show the "typing..." / "sending photo..." indicator. */
267
- sendChatAction(action = "typing") {
313
+ async sendChatAction(action = "typing") {
314
+ const peer = await this._peer();
268
315
  const actions = {
269
316
  typing: new Api.SendMessageTypingAction(),
270
317
  photo: new Api.SendMessageUploadPhotoAction({ progress: 0 }),
@@ -275,7 +322,7 @@ class Context {
275
322
  };
276
323
  return this.client.invoke(
277
324
  new Api.messages.SetTyping({
278
- peer: this.chatId,
325
+ peer,
279
326
  action: actions[action] || actions.typing,
280
327
  })
281
328
  );