@rilong/grammyjs-conversations-esm 2.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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/out/conversation.d.ts +885 -0
- package/out/conversation.js +832 -0
- package/out/deps.node.d.ts +2 -0
- package/out/deps.node.js +1 -0
- package/out/engine.d.ts +212 -0
- package/out/engine.js +238 -0
- package/out/form.d.ts +530 -0
- package/out/form.js +598 -0
- package/out/menu.d.ts +593 -0
- package/out/menu.js +698 -0
- package/out/mod.d.ts +8 -0
- package/out/mod.js +8 -0
- package/out/nope.d.ts +16 -0
- package/out/nope.js +35 -0
- package/out/plugin.d.ts +678 -0
- package/out/plugin.js +578 -0
- package/out/resolve.d.ts +43 -0
- package/out/resolve.js +21 -0
- package/out/state.d.ts +147 -0
- package/out/state.js +125 -0
- package/out/storage.d.ts +169 -0
- package/out/storage.js +105 -0
- package/package.json +43 -0
@@ -0,0 +1,832 @@
|
|
1
|
+
import { Context, } from "./deps.node.js";
|
2
|
+
import { ConversationForm } from "./form.js";
|
3
|
+
import { ConversationMenuPool } from "./menu.js";
|
4
|
+
/**
|
5
|
+
* A conversation handle lets you control the conversation, such as waiting for
|
6
|
+
* updates, skipping them, halting the conversation, and much more. It is the
|
7
|
+
* first parameter in each conversation builder function and provides the core
|
8
|
+
* features of this plugin.
|
9
|
+
*
|
10
|
+
* ```ts
|
11
|
+
* async function exmaple(conversation, ctx) {
|
12
|
+
* // ^ this is an instance of this class
|
13
|
+
*
|
14
|
+
* // This is how you can wait for updates:
|
15
|
+
* ctx = await conversation.wait()
|
16
|
+
* }
|
17
|
+
* ```
|
18
|
+
*
|
19
|
+
* Be sure to consult this plugin's documentation:
|
20
|
+
* https://grammy.dev/plugins/conversations
|
21
|
+
*/
|
22
|
+
export class Conversation {
|
23
|
+
/**
|
24
|
+
* Constructs a new conversation handle.
|
25
|
+
*
|
26
|
+
* This is called internally in order to construct the first argument for a
|
27
|
+
* conversation builder function. You typically don't need to construct this
|
28
|
+
* class yourself.
|
29
|
+
*
|
30
|
+
* @param controls Controls for the underlying replay engine
|
31
|
+
* @param hydrate Context construction callback
|
32
|
+
* @param escape Callback to support outside context objects in `external`
|
33
|
+
* @param plugins Middleware to hydrate context objects
|
34
|
+
* @param options Additional configuration options
|
35
|
+
*/
|
36
|
+
constructor(controls, hydrate, escape, plugins, options) {
|
37
|
+
this.controls = controls;
|
38
|
+
this.hydrate = hydrate;
|
39
|
+
this.escape = escape;
|
40
|
+
this.plugins = plugins;
|
41
|
+
this.options = options;
|
42
|
+
/** `true` if `external` is currently running, `false` otherwise */
|
43
|
+
this.insideExternal = false;
|
44
|
+
this.menuPool = new ConversationMenuPool();
|
45
|
+
this.combineAnd = makeAndCombiner(this);
|
46
|
+
/**
|
47
|
+
* A namespace full of various utitilies for building forms.
|
48
|
+
*
|
49
|
+
* Typically, `wait` calls return context objects. Optionally, these context
|
50
|
+
* objects can be accepted or rejected based on validation, such as with
|
51
|
+
* `waitFor` which only returns context objects matching a given filter
|
52
|
+
* query.
|
53
|
+
*
|
54
|
+
* Forms add another level of convenience on top of this. They no longer
|
55
|
+
* require you to deal with context objects. Each form field performs both
|
56
|
+
* validation and selection. This means that it picks out certain property
|
57
|
+
* from the context object—such as the message text—and returns this
|
58
|
+
* property directly.
|
59
|
+
*
|
60
|
+
* As an example, here is how you can wait for a number using the form field
|
61
|
+
* `.number`.
|
62
|
+
*
|
63
|
+
* ```ts
|
64
|
+
* // Wait for a number
|
65
|
+
* const n = await conversation.form.number()
|
66
|
+
* // Send back its square
|
67
|
+
* await ctx.reply(`The square of ${n} is ${n * n}!`)
|
68
|
+
* ```
|
69
|
+
*
|
70
|
+
* There are many more form fields that let you wait for virtually any type
|
71
|
+
* of message content.
|
72
|
+
*
|
73
|
+
* All form fields give you the option to perform an action if the
|
74
|
+
* validation fails by accepting an `otherwise` function. This is similar to
|
75
|
+
* filtered wait calls.
|
76
|
+
*
|
77
|
+
* ```ts
|
78
|
+
* const text = await conversation.form.select(["Yes", "No"], {
|
79
|
+
* otherwise: ctx => ctx.reply("Please send Yes or No.")
|
80
|
+
* })
|
81
|
+
* ```
|
82
|
+
*
|
83
|
+
* In addition, all form fields give you the option to perform some action
|
84
|
+
* when a value is accepted. For example, this is how you can delete
|
85
|
+
* incoming messages.
|
86
|
+
*
|
87
|
+
* ```ts
|
88
|
+
* const text = await conversation.form.select(["Yes", "No"], {
|
89
|
+
* action: ctx => ctx.deleteMessage()
|
90
|
+
* })
|
91
|
+
* ```
|
92
|
+
*
|
93
|
+
* Note that either `otherwise` or `action` will be called, but never both
|
94
|
+
* for the same update.
|
95
|
+
*/
|
96
|
+
this.form = new ConversationForm(this);
|
97
|
+
}
|
98
|
+
/**
|
99
|
+
* Waits for a new update and returns the corresponding context object as
|
100
|
+
* soon as it arrives.
|
101
|
+
*
|
102
|
+
* Note that wait calls terminate the conversation function, save the state
|
103
|
+
* of execution, and only resolve when the conversation is replayed. If this
|
104
|
+
* is not obvious to you, it means that you probably should read [the
|
105
|
+
* documentation of this plugin](https://grammy.dev/plugins/conversations)
|
106
|
+
* in order to avoid common pitfalls.
|
107
|
+
*
|
108
|
+
* You can pass a timeout in the optional options object. This lets you
|
109
|
+
* terminate the conversation automatically if the update arrives too late.
|
110
|
+
*
|
111
|
+
* @param options Optional options for wait timeouts etc
|
112
|
+
*/
|
113
|
+
wait(options = {}) {
|
114
|
+
if (this.insideExternal) {
|
115
|
+
throw new Error("Cannot wait for updates from inside `external`, or concurrently to it! \
|
116
|
+
First return your data from `external` and then resume update handling using `wait` calls.");
|
117
|
+
}
|
118
|
+
const makeWait = async () => {
|
119
|
+
var _a;
|
120
|
+
// obtain update
|
121
|
+
const limit = "maxMilliseconds" in options
|
122
|
+
? options.maxMilliseconds
|
123
|
+
: this.options.maxMillisecondsToWait;
|
124
|
+
const key = (_a = options.collationKey) !== null && _a !== void 0 ? _a : "wait";
|
125
|
+
const before = limit !== undefined && await this.now();
|
126
|
+
const update = await this.controls.interrupt(key);
|
127
|
+
if (before !== false) {
|
128
|
+
const after = await this.now();
|
129
|
+
if (after - before >= limit) {
|
130
|
+
await this.halt({ next: true });
|
131
|
+
}
|
132
|
+
}
|
133
|
+
// convert to context object
|
134
|
+
const ctx = this.hydrate(update);
|
135
|
+
// prepare context for menus
|
136
|
+
const { handleClicks } = this.menuPool.install(ctx);
|
137
|
+
// run plugins
|
138
|
+
let pluginsCalledNext = false;
|
139
|
+
await this.plugins(ctx, () => {
|
140
|
+
pluginsCalledNext = true;
|
141
|
+
return Promise.resolve();
|
142
|
+
});
|
143
|
+
// If a plugin decided to handle the update (did not call `next`),
|
144
|
+
// then we recurse and simply wait for another update.
|
145
|
+
if (!pluginsCalledNext)
|
146
|
+
return await this.wait(options);
|
147
|
+
// run menus
|
148
|
+
const { next: menuCalledNext } = await handleClicks();
|
149
|
+
// If a menu decided to handle the update (did not call `next`),
|
150
|
+
// then we recurse and simply wait for another update.
|
151
|
+
if (!menuCalledNext)
|
152
|
+
return await this.wait(options);
|
153
|
+
return ctx;
|
154
|
+
};
|
155
|
+
return this.combineAnd(makeWait());
|
156
|
+
}
|
157
|
+
waitUntil(predicate, opts = {}) {
|
158
|
+
const makeWait = async () => {
|
159
|
+
const { otherwise, next, ...waitOptions } = opts;
|
160
|
+
const ctx = await this.wait({
|
161
|
+
collationKey: "until",
|
162
|
+
...waitOptions,
|
163
|
+
});
|
164
|
+
if (!await predicate(ctx)) {
|
165
|
+
await (otherwise === null || otherwise === void 0 ? void 0 : otherwise(ctx));
|
166
|
+
await this.skip(next === undefined ? {} : { next });
|
167
|
+
}
|
168
|
+
return ctx;
|
169
|
+
};
|
170
|
+
return this.combineAnd(makeWait());
|
171
|
+
}
|
172
|
+
/**
|
173
|
+
* Performs a filtered wait call that is defined by a given negated
|
174
|
+
* predicate. In other words, this method waits for an update, and calls
|
175
|
+
* `skip` if the received context object passed validation performed by the
|
176
|
+
* given predicate function. That is the exact same thigs as calling
|
177
|
+
* {@link Conversation.waitUntil} but with the predicate function being
|
178
|
+
* negated.
|
179
|
+
*
|
180
|
+
* If a context object is discarded (the predicate function returns `true`
|
181
|
+
* for it), you can perform any action by specifying `otherwise` in the
|
182
|
+
* options.
|
183
|
+
*
|
184
|
+
* ```ts
|
185
|
+
* const ctx = await conversation.waitUnless(ctx => ctx.msg?.text?.endsWith("grammY"), {
|
186
|
+
* otherwise: ctx => ctx.reply("Send a message that does not end with grammY!")
|
187
|
+
* })
|
188
|
+
* ```
|
189
|
+
*
|
190
|
+
* You can combine calls to `waitUnless` with other filtered wait calls by
|
191
|
+
* chaining them.
|
192
|
+
*
|
193
|
+
* ```ts
|
194
|
+
* const ctx = await conversation.waitUnless(ctx => ctx.msg?.text?.endsWith("grammY"))
|
195
|
+
* .andFor("::hashtag")
|
196
|
+
* ```
|
197
|
+
*
|
198
|
+
* @param predicate A predicate function to discard context objects
|
199
|
+
* @param opts Optional options object
|
200
|
+
*/
|
201
|
+
waitUnless(predicate, opts) {
|
202
|
+
return this.combineAnd(this.waitUntil(async (ctx) => !await predicate(ctx), {
|
203
|
+
collationKey: "unless",
|
204
|
+
...opts,
|
205
|
+
}));
|
206
|
+
}
|
207
|
+
/**
|
208
|
+
* Performs a filtered wait call that is defined by a filter query. In other
|
209
|
+
* words, this method waits for an update, and calls `skip` if the received
|
210
|
+
* context object does not match the filter query. This uses the same logic
|
211
|
+
* as `bot.on`.
|
212
|
+
*
|
213
|
+
* If a context object is discarded, you can perform any action by
|
214
|
+
* specifying `otherwise` in the options.
|
215
|
+
*
|
216
|
+
* ```ts
|
217
|
+
* const ctx = await conversation.waitFor(":text", {
|
218
|
+
* otherwise: ctx => ctx.reply("Please send a text message!")
|
219
|
+
* })
|
220
|
+
* // Type inference works:
|
221
|
+
* const text = ctx.msg.text;
|
222
|
+
* ```
|
223
|
+
*
|
224
|
+
* You can combine calls to `waitFor` with other filtered wait calls by
|
225
|
+
* chaining them.
|
226
|
+
*
|
227
|
+
* ```ts
|
228
|
+
* const ctx = await conversation.waitFor(":text").andFor("::hashtag")
|
229
|
+
* ```
|
230
|
+
*
|
231
|
+
* @param query A filter query to match
|
232
|
+
* @param opts Optional options object
|
233
|
+
*/
|
234
|
+
waitFor(query, opts) {
|
235
|
+
return this.combineAnd(this.waitUntil(Context.has.filterQuery(query), {
|
236
|
+
collationKey: Array.isArray(query) ? query.join(",") : query,
|
237
|
+
...opts,
|
238
|
+
}));
|
239
|
+
}
|
240
|
+
/**
|
241
|
+
* Performs a filtered wait call that is defined by a hears filter. In other
|
242
|
+
* words, this method waits for an update, and calls `skip` if the received
|
243
|
+
* context object does not contain text that matches the given text or
|
244
|
+
* regular expression. This uses the same logic as `bot.hears`.
|
245
|
+
*
|
246
|
+
* If a context object is discarded, you can perform any action by
|
247
|
+
* specifying `otherwise` in the options.
|
248
|
+
*
|
249
|
+
* ```ts
|
250
|
+
* const ctx = await conversation.waitForHears(["yes", "no"], {
|
251
|
+
* otherwise: ctx => ctx.reply("Please send yes or no!")
|
252
|
+
* })
|
253
|
+
* // Type inference works:
|
254
|
+
* const answer = ctx.match
|
255
|
+
* ```
|
256
|
+
*
|
257
|
+
* You can combine calls to `waitForHears` with other filtered wait calls by
|
258
|
+
* chaining them. For instance, this can be used to only receive text from
|
259
|
+
* text messages—not including channel posts or media captions.
|
260
|
+
*
|
261
|
+
* ```ts
|
262
|
+
* const ctx = await conversation.waitForHears(["yes", "no"])
|
263
|
+
* .andFor("message:text")
|
264
|
+
* const text = ctx.message.text
|
265
|
+
* ```
|
266
|
+
*
|
267
|
+
* @param trigger The text to look for
|
268
|
+
* @param opts Optional options object
|
269
|
+
*/
|
270
|
+
waitForHears(trigger, opts) {
|
271
|
+
return this.combineAnd(this.waitUntil(Context.has.text(trigger), {
|
272
|
+
collationKey: "hears",
|
273
|
+
...opts,
|
274
|
+
}));
|
275
|
+
}
|
276
|
+
/**
|
277
|
+
* Performs a filtered wait call that is defined by a command filter. In
|
278
|
+
* other words, this method waits for an update, and calls `skip` if the
|
279
|
+
* received context object does not contain the expected command. This uses
|
280
|
+
* the same logic as `bot.command`.
|
281
|
+
*
|
282
|
+
* If a context object is discarded, you can perform any action by
|
283
|
+
* specifying `otherwise` in the options.
|
284
|
+
*
|
285
|
+
* ```ts
|
286
|
+
* const ctx = await conversation.waitForCommand("start", {
|
287
|
+
* otherwise: ctx => ctx.reply("Please send /start!")
|
288
|
+
* })
|
289
|
+
* // Type inference works for deep links:
|
290
|
+
* const args = ctx.match
|
291
|
+
* ```
|
292
|
+
*
|
293
|
+
* You can combine calls to `waitForCommand` with other filtered wait calls
|
294
|
+
* by chaining them. For instance, this can be used to only receive commands
|
295
|
+
* from text messages—not including channel posts.
|
296
|
+
*
|
297
|
+
* ```ts
|
298
|
+
* const ctx = await conversation.waitForCommand("start")
|
299
|
+
* .andFor("message")
|
300
|
+
* ```
|
301
|
+
*
|
302
|
+
* @param command The command to look for
|
303
|
+
* @param opts Optional options object
|
304
|
+
*/
|
305
|
+
waitForCommand(command, opts) {
|
306
|
+
return this.combineAnd(this.waitUntil(Context.has.command(command), {
|
307
|
+
collationKey: "command",
|
308
|
+
...opts,
|
309
|
+
}));
|
310
|
+
}
|
311
|
+
/**
|
312
|
+
* Performs a filtered wait call that is defined by a reaction filter. In
|
313
|
+
* other words, this method waits for an update, and calls `skip` if the
|
314
|
+
* received context object does not contain the expected reaction update.
|
315
|
+
* This uses the same logic as `bot.reaction`.
|
316
|
+
*
|
317
|
+
* If a context object is discarded, you can perform any action by
|
318
|
+
* specifying `otherwise` in the options.
|
319
|
+
*
|
320
|
+
* ```ts
|
321
|
+
* const ctx = await conversation.waitForReaction('👍', {
|
322
|
+
* otherwise: ctx => ctx.reply("Please upvote a message!")
|
323
|
+
* })
|
324
|
+
* // Type inference works:
|
325
|
+
* const args = ctx.messageReaction
|
326
|
+
* ```
|
327
|
+
*
|
328
|
+
* You can combine calls to `waitForReaction` with other filtered wait calls
|
329
|
+
* by chaining them.
|
330
|
+
*
|
331
|
+
* ```ts
|
332
|
+
* const ctx = await conversation.waitForReaction('👍')
|
333
|
+
* .andFrom(ADMIN_USER_ID)
|
334
|
+
* ```
|
335
|
+
*
|
336
|
+
* @param reaction The reaction to look for
|
337
|
+
* @param opts Optional options object
|
338
|
+
*/
|
339
|
+
waitForReaction(reaction, opts) {
|
340
|
+
return this.combineAnd(this.waitUntil(Context.has.reaction(reaction), {
|
341
|
+
collationKey: "reaction",
|
342
|
+
...opts,
|
343
|
+
}));
|
344
|
+
}
|
345
|
+
/**
|
346
|
+
* Performs a filtered wait call that is defined by a callback query filter.
|
347
|
+
* In other words, this method waits for an update, and calls `skip` if the
|
348
|
+
* received context object does not contain the expected callback query
|
349
|
+
* update. This uses the same logic as `bot.callbackQuery`.
|
350
|
+
*
|
351
|
+
* If a context object is discarded, you can perform any action by
|
352
|
+
* specifying `otherwise` in the options.
|
353
|
+
*
|
354
|
+
* ```ts
|
355
|
+
* const ctx = await conversation.waitForCallbackQuery(/button-\d+/, {
|
356
|
+
* otherwise: ctx => ctx.reply("Please click a button!")
|
357
|
+
* })
|
358
|
+
* // Type inference works:
|
359
|
+
* const data = ctx.callbackQuery.data
|
360
|
+
* ```
|
361
|
+
*
|
362
|
+
* You can combine calls to `waitForCallbackQuery` with other filtered wait
|
363
|
+
* calls by chaining them.
|
364
|
+
*
|
365
|
+
* ```ts
|
366
|
+
* const ctx = await conversation.waitForCallbackQuery('data')
|
367
|
+
* .andFrom(ADMIN_USER_ID)
|
368
|
+
* ```
|
369
|
+
*
|
370
|
+
* @param trigger The string to look for in the payload
|
371
|
+
* @param opts Optional options object
|
372
|
+
*/
|
373
|
+
waitForCallbackQuery(trigger, opts) {
|
374
|
+
return this.combineAnd(this.waitUntil(Context.has.callbackQuery(trigger), {
|
375
|
+
collationKey: "callback",
|
376
|
+
...opts,
|
377
|
+
}));
|
378
|
+
}
|
379
|
+
/**
|
380
|
+
* Performs a filtered wait call that is defined by a game query filter. In
|
381
|
+
* other words, this method waits for an update, and calls `skip` if the
|
382
|
+
* received context object does not contain the expected game query update.
|
383
|
+
* This uses the same logic as `bot.gameQuery`.
|
384
|
+
*
|
385
|
+
* If a context object is discarded, you can perform any action by
|
386
|
+
* specifying `otherwise` in the options.
|
387
|
+
*
|
388
|
+
* ```ts
|
389
|
+
* const ctx = await conversation.waitForGameQuery(/game-\d+/, {
|
390
|
+
* otherwise: ctx => ctx.reply("Please play a game!")
|
391
|
+
* })
|
392
|
+
* // Type inference works:
|
393
|
+
* const data = ctx.callbackQuery.game_short_name
|
394
|
+
* ```
|
395
|
+
*
|
396
|
+
* You can combine calls to `waitForGameQuery` with other filtered wait
|
397
|
+
* calls by chaining them.
|
398
|
+
*
|
399
|
+
* ```ts
|
400
|
+
* const ctx = await conversation.waitForGameQuery('data')
|
401
|
+
* .andFrom(ADMIN_USER_ID)
|
402
|
+
* ```
|
403
|
+
*
|
404
|
+
* @param trigger The string to look for in the payload
|
405
|
+
* @param opts Optional options object
|
406
|
+
*/
|
407
|
+
waitForGameQuery(trigger, opts) {
|
408
|
+
return this.combineAnd(this.waitUntil(Context.has.gameQuery(trigger), {
|
409
|
+
collationKey: "game",
|
410
|
+
...opts,
|
411
|
+
}));
|
412
|
+
}
|
413
|
+
/**
|
414
|
+
* Performs a filtered wait call that is defined by a user-specific filter.
|
415
|
+
* In other words, this method waits for an update, and calls `skip` if the
|
416
|
+
* received context object was not triggered by the given user.
|
417
|
+
*
|
418
|
+
* If a context object is discarded, you can perform any action by
|
419
|
+
* specifying `otherwise` in the options.
|
420
|
+
*
|
421
|
+
* ```ts
|
422
|
+
* const ctx = await conversation.waitFrom(targetUser, {
|
423
|
+
* otherwise: ctx => ctx.reply("I did not mean you!")
|
424
|
+
* })
|
425
|
+
* // Type inference works:
|
426
|
+
* const user = ctx.from.first_name
|
427
|
+
* ```
|
428
|
+
*
|
429
|
+
* You can combine calls to `waitFrom` with other filtered wait calls by
|
430
|
+
* chaining them.
|
431
|
+
*
|
432
|
+
* ```ts
|
433
|
+
* const ctx = await conversation.waitFrom(targetUser).andFor(":text")
|
434
|
+
* ```
|
435
|
+
*
|
436
|
+
* @param user The user or user identifer to look for
|
437
|
+
* @param opts Optional options object
|
438
|
+
*/
|
439
|
+
waitFrom(user, opts) {
|
440
|
+
const id = typeof user === "number" ? user : user.id;
|
441
|
+
return this.combineAnd(this.waitUntil((ctx) => { var _a; return ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) === id; }, { collationKey: `from-${id}`, ...opts }));
|
442
|
+
}
|
443
|
+
/**
|
444
|
+
* Performs a filtered wait call that is defined by a message reply. In
|
445
|
+
* other words, this method waits for an update, and calls `skip` if the
|
446
|
+
* received context object does not contain a reply to a given message.
|
447
|
+
*
|
448
|
+
* If a context object is discarded, you can perform any action by
|
449
|
+
* specifying `otherwise` in the options.
|
450
|
+
*
|
451
|
+
* ```ts
|
452
|
+
* const ctx = await conversation.waitForReplyTo(message, {
|
453
|
+
* otherwise: ctx => ctx.reply("Please reply to this message!", {
|
454
|
+
* reply_parameters: { message_id: message.message_id }
|
455
|
+
* })
|
456
|
+
* })
|
457
|
+
* // Type inference works:
|
458
|
+
* const id = ctx.msg.message_id
|
459
|
+
* ```
|
460
|
+
*
|
461
|
+
* You can combine calls to `waitForReplyTo` with other filtered wait calls
|
462
|
+
* by chaining them.
|
463
|
+
*
|
464
|
+
* ```ts
|
465
|
+
* const ctx = await conversation.waitForReplyTo(message).andFor(":text")
|
466
|
+
* ```
|
467
|
+
*
|
468
|
+
* @param message_id The message identifer or object to look for in a reply
|
469
|
+
* @param opts Optional options object
|
470
|
+
*/
|
471
|
+
waitForReplyTo(message_id, opts) {
|
472
|
+
const id = typeof message_id === "number"
|
473
|
+
? message_id
|
474
|
+
: message_id.message_id;
|
475
|
+
return this.combineAnd(this.waitUntil((ctx) => {
|
476
|
+
var _a, _b, _c, _d;
|
477
|
+
return ((_b = (_a = ctx.message) === null || _a === void 0 ? void 0 : _a.reply_to_message) === null || _b === void 0 ? void 0 : _b.message_id) === id ||
|
478
|
+
((_d = (_c = ctx.channelPost) === null || _c === void 0 ? void 0 : _c.reply_to_message) === null || _d === void 0 ? void 0 : _d.message_id) === id;
|
479
|
+
}, { collationKey: `reply-${id}`, ...opts }));
|
480
|
+
}
|
481
|
+
/**
|
482
|
+
* Skips the current update. The current update is the update that was
|
483
|
+
* received in the last wait call.
|
484
|
+
*
|
485
|
+
* In a sense, this will undo receiving an update. The replay logs will be
|
486
|
+
* reset so it will look like the conversation had never received the update
|
487
|
+
* in the first place. Note, however, that any API calls performs between
|
488
|
+
* wait and skip are not going to be reversed. In particular, messages will
|
489
|
+
* not be unsent.
|
490
|
+
*
|
491
|
+
* By default, skipping an update drops it. This means that no other
|
492
|
+
* handlers (including downstream middleware) will run. However, if this
|
493
|
+
* conversation is marked as parallel, skip will behave differently and
|
494
|
+
* resume middleware execution by default. This is needed for other parallel
|
495
|
+
* conversations with the same or a different identifier to receive the
|
496
|
+
* update.
|
497
|
+
*
|
498
|
+
* This behavior can be overridden by passing `{ next: true }` or `{ next:
|
499
|
+
* false }` to skip.
|
500
|
+
*
|
501
|
+
* If several wait calls are used concurrently inside the same conversation,
|
502
|
+
* they will resolve one after another until one of them does not skip the
|
503
|
+
* update. The conversation will only skip an update when all concurrent
|
504
|
+
* wait calls skip the update. Specifying `next` for a skip call that is not
|
505
|
+
* the final skip call has no effect.
|
506
|
+
*
|
507
|
+
* @param options Optional options to control middleware resumption
|
508
|
+
*/
|
509
|
+
async skip(options = {}) {
|
510
|
+
const next = "next" in options ? options.next : this.options.parallel;
|
511
|
+
return await this.controls.cancel(next ? "skip" : "drop");
|
512
|
+
}
|
513
|
+
/**
|
514
|
+
* Calls any exit handlers if installed, and then terminates the
|
515
|
+
* conversation immediately. This method never returns.
|
516
|
+
*
|
517
|
+
* By default, this will consume the update. Pass `{ next: true }` to make
|
518
|
+
* sure that downstream middleware is called.
|
519
|
+
*
|
520
|
+
* @param options Optional options to control middleware resumption
|
521
|
+
*/
|
522
|
+
async halt(options = {}) {
|
523
|
+
var _a, _b;
|
524
|
+
await ((_b = (_a = this.options).onHalt) === null || _b === void 0 ? void 0 : _b.call(_a));
|
525
|
+
return await this.controls.cancel(options.next ? "kill" : "halt");
|
526
|
+
}
|
527
|
+
/**
|
528
|
+
* Creates a new checkpoint at the current point of the conversation.
|
529
|
+
*
|
530
|
+
* This checkpoint can be passed to `rewind` in order to go back in the
|
531
|
+
* conversation and resume it from an earlier point.
|
532
|
+
*
|
533
|
+
* ```ts
|
534
|
+
* const check = conversation.checkpoint();
|
535
|
+
*
|
536
|
+
* // Later:
|
537
|
+
* await conversation.rewind(check);
|
538
|
+
* ```
|
539
|
+
*/
|
540
|
+
checkpoint() {
|
541
|
+
return this.controls.checkpoint();
|
542
|
+
}
|
543
|
+
/**
|
544
|
+
* Rewinds the conversation to a previous point and continues execution from
|
545
|
+
* there. This point is specified by a checkpoint that can be created by
|
546
|
+
* calling {@link Conversation.checkpoint}.
|
547
|
+
*
|
548
|
+
* ```ts
|
549
|
+
* const check = conversation.checkpoint();
|
550
|
+
*
|
551
|
+
* // Later:
|
552
|
+
* await conversation.rewind(check);
|
553
|
+
* ```
|
554
|
+
*
|
555
|
+
* @param checkpoint A previously created checkpoint
|
556
|
+
*/
|
557
|
+
async rewind(checkpoint) {
|
558
|
+
return await this.controls.cancel(checkpoint);
|
559
|
+
}
|
560
|
+
/**
|
561
|
+
* Runs a function outside of the replay engine. This provides a safe way to
|
562
|
+
* perform side-effects such as database communication, disk operations,
|
563
|
+
* session access, file downloads, requests to external APIs, randomness,
|
564
|
+
* time-based functions, and more. **It requires any data obtained from the
|
565
|
+
* outside to be serializable.**
|
566
|
+
*
|
567
|
+
* Remember that a conversation function is not executed like a normal
|
568
|
+
* JavaScript function. Instead, it is often interrupted and replayed,
|
569
|
+
* sometimes many times for the same update. If this is not obvious to you,
|
570
|
+
* it means that you probably should read [the documentation of this
|
571
|
+
* plugin](https://grammy.dev/plugins/conversations) in order to avoid
|
572
|
+
* common pitfalls.
|
573
|
+
*
|
574
|
+
* For instance, if you want to access to your database, you only want to
|
575
|
+
* read or write data once, rather than doing it once per replay. `external`
|
576
|
+
* provides an escape hatch to this situation. You can wrap your database
|
577
|
+
* call inside `external` to mark it as something that performs
|
578
|
+
* side-effects. The replay engine inside the conversations plugin will then
|
579
|
+
* make sure to only execute this operation once. This looks as follows.
|
580
|
+
*
|
581
|
+
* ```ts
|
582
|
+
* // Read from database
|
583
|
+
* const data = await conversation.external(async () => {
|
584
|
+
* return await readFromDatabase()
|
585
|
+
* })
|
586
|
+
*
|
587
|
+
* // Write to database
|
588
|
+
* await conversation.external(async () => {
|
589
|
+
* await writeToDatabase(data)
|
590
|
+
* })
|
591
|
+
* ```
|
592
|
+
*
|
593
|
+
* When `external` is called, it returns whichever data the given callback
|
594
|
+
* function returns. Note that this data has to be persisted by the plugin,
|
595
|
+
* so you have to make sure that it can be serialized. The data will be
|
596
|
+
* stored in the storage backend you provided when installing the
|
597
|
+
* conversations plugin via `bot.use`. In particular, it does not work well
|
598
|
+
* to return objects created by an ORM, as these objects have functions
|
599
|
+
* installed on them which will be lost during serialization.
|
600
|
+
*
|
601
|
+
* As a rule of thumb, imagine that all data from `external` is passed
|
602
|
+
* through `JSON.parse(JSON.stringify(data))` (even though this is not what
|
603
|
+
* actually happens under the hood).
|
604
|
+
*
|
605
|
+
* The callback function passed to `external` receives the outside context
|
606
|
+
* object from the current middleware pass. This lets you access properties
|
607
|
+
* on the context object that are only present in the outside middleware
|
608
|
+
* system, but that have not been installed on the context objects inside a
|
609
|
+
* conversation. For example, you can access your session data this way.
|
610
|
+
*
|
611
|
+
* ```ts
|
612
|
+
* // Read from session
|
613
|
+
* const data = await conversation.external((ctx) => {
|
614
|
+
* return ctx.session.data
|
615
|
+
* })
|
616
|
+
*
|
617
|
+
* // Write to session
|
618
|
+
* await conversation.external((ctx) => {
|
619
|
+
* ctx.session.data = data
|
620
|
+
* })
|
621
|
+
* ```
|
622
|
+
*
|
623
|
+
* Note that while a call to `external` is running, you cannot do any of the
|
624
|
+
* following things.
|
625
|
+
*
|
626
|
+
* - start a concurrent call to `external` from the same conversation
|
627
|
+
* - start a nested call to `external` from the same conversation
|
628
|
+
* - start a Bot API call from the same conversation
|
629
|
+
*
|
630
|
+
* Naturally, it is possible to have several concurrent calls to `externals`
|
631
|
+
* if they happen in unrelated chats. This still means that you should keep
|
632
|
+
* the code inside `external` to a minimum and actually only perform the
|
633
|
+
* desired side-effect itself.
|
634
|
+
*
|
635
|
+
* If you want to return data from `external` that cannot be serialized, you
|
636
|
+
* can specify a custom serialization function. This allows you choose a
|
637
|
+
* different intermediate data representation during storage than what is
|
638
|
+
* present at runtime.
|
639
|
+
*
|
640
|
+
* ```ts
|
641
|
+
* // Read bigint from an API but persist it as a string
|
642
|
+
* const largeNumber: bigint = await conversation.external({
|
643
|
+
* task: () => fetchCoolBigIntFromTheInternet(),
|
644
|
+
* beforeStore: (largeNumber) => String(largeNumber),
|
645
|
+
* afterLoad: (str) => BigInt(str),
|
646
|
+
* })
|
647
|
+
* ```
|
648
|
+
*
|
649
|
+
* Note how we read a bigint from the internet, but we convert it to string
|
650
|
+
* during persistence. This now allows us to use a storage adapter that only
|
651
|
+
* handles strings but does not need to support the bigint type.
|
652
|
+
*
|
653
|
+
* @param op An operation to perform outside of the conversation
|
654
|
+
*/
|
655
|
+
// deno-lint-ignore no-explicit-any
|
656
|
+
async external(op) {
|
657
|
+
// Make sure that no other ops are performed concurrently (or from
|
658
|
+
// within the handler) because they will not be performed during a
|
659
|
+
// replay so they will be missing from the logs then, which clogs up the
|
660
|
+
// replay. This detection must be done here because this is the only
|
661
|
+
// place where misuse can be detected properly. The replay engine cannot
|
662
|
+
// discover that on its own because otherwise it would not support
|
663
|
+
// concurrent ops at all, which is undesired.
|
664
|
+
if (this.insideExternal) {
|
665
|
+
throw new Error("Cannot perform nested or concurrent calls to `external`!");
|
666
|
+
}
|
667
|
+
const { task, afterLoad = (x) => x, afterLoadError = (e) => e, beforeStore = (x) => x, beforeStoreError = (e) => e, } = typeof op === "function" ? { task: op } : op;
|
668
|
+
// Prepare values before storing them
|
669
|
+
const action = async () => {
|
670
|
+
this.insideExternal = true;
|
671
|
+
try {
|
672
|
+
// We perform an unsafe cast to the context type used in the
|
673
|
+
// surrounding middleware system. Technically, we could drag
|
674
|
+
// this type along from outside by adding an extra type
|
675
|
+
// parameter everywhere, but this makes all types too cumbersome
|
676
|
+
// to work with for bot developers. The benefits of this
|
677
|
+
// massively reduced complexity outweight the potential benefits
|
678
|
+
// of slightly stricter types for `external`.
|
679
|
+
const ret = await this.escape((ctx) => task(ctx));
|
680
|
+
return { ok: true, ret: await beforeStore(ret) };
|
681
|
+
}
|
682
|
+
catch (e) {
|
683
|
+
return { ok: false, err: await beforeStoreError(e) };
|
684
|
+
}
|
685
|
+
finally {
|
686
|
+
this.insideExternal = false;
|
687
|
+
}
|
688
|
+
};
|
689
|
+
// Recover values after loading them
|
690
|
+
const ret = await this.controls.action(action, "external");
|
691
|
+
if (ret.ok) {
|
692
|
+
return await afterLoad(ret.ret);
|
693
|
+
}
|
694
|
+
else {
|
695
|
+
throw await afterLoadError(ret.err);
|
696
|
+
}
|
697
|
+
}
|
698
|
+
/**
|
699
|
+
* Takes `Date.now()` once when reached, and returns the same value during
|
700
|
+
* every replay. Prefer this over calling `Date.now()` directly.
|
701
|
+
*/
|
702
|
+
async now() {
|
703
|
+
return await this.external(() => Date.now());
|
704
|
+
}
|
705
|
+
/**
|
706
|
+
* Takes `Math.random()` once when reached, and returns the same value
|
707
|
+
* during every replay. Prefer this over calling `Math.random()` directly.
|
708
|
+
*/
|
709
|
+
async random() {
|
710
|
+
return await this.external(() => Math.random());
|
711
|
+
}
|
712
|
+
/**
|
713
|
+
* Calls `console.log` only the first time it is reached, but not during
|
714
|
+
* subsequent replays. Prefer this over calling `console.log` directly.
|
715
|
+
*/
|
716
|
+
async log(...data) {
|
717
|
+
await this.external(() => console.log(...data));
|
718
|
+
}
|
719
|
+
/**
|
720
|
+
* Calls `console.error` only the first time it is reached, but not during
|
721
|
+
* subsequent replays. Prefer this over calling `console.error` directly.
|
722
|
+
*/
|
723
|
+
async error(...data) {
|
724
|
+
await this.external(() => console.error(...data));
|
725
|
+
}
|
726
|
+
/**
|
727
|
+
* Creates a new conversational menu.
|
728
|
+
*
|
729
|
+
* A conversational menu is a an interactive inline keyboard that is sent to
|
730
|
+
* the user from within a conversation.
|
731
|
+
*
|
732
|
+
* ```ts
|
733
|
+
* const menu = conversation.menu()
|
734
|
+
* .text("Send message", ctx => ctx.reply("Hi!"))
|
735
|
+
* .text("Close", ctx => ctx.menu.close())
|
736
|
+
*
|
737
|
+
* await ctx.reply("Menu message", { reply_markup: menu })
|
738
|
+
* ```
|
739
|
+
*
|
740
|
+
* If a menu identifier is specified, conversational menus enable seamless
|
741
|
+
* navigation.
|
742
|
+
*
|
743
|
+
* ```ts
|
744
|
+
* const menu = conversation.menu("root")
|
745
|
+
* .submenu("Open submenu", ctx => ctx.editMessageText("submenu"))
|
746
|
+
* .text("Close", ctx => ctx.menu.close())
|
747
|
+
* conversation.menu("child", { parent: "root" })
|
748
|
+
* .back("Go back", ctx => ctx.editMessageText("Root menu"))
|
749
|
+
*
|
750
|
+
* await ctx.reply("Root menu", { reply_markup: menu })
|
751
|
+
* ```
|
752
|
+
*
|
753
|
+
* You can also interact with the conversation from inside button handlers.
|
754
|
+
*
|
755
|
+
* ```ts
|
756
|
+
* let name = ""
|
757
|
+
* const menu = conversation.menu()
|
758
|
+
* .text("Set name", async ctx => {
|
759
|
+
* await ctx.reply("What's your name?")
|
760
|
+
* name = await conversation.form.text()
|
761
|
+
* await ctx.editMessageText(name)
|
762
|
+
* })
|
763
|
+
* .text("Clear name", ctx => {
|
764
|
+
* name = ""
|
765
|
+
* await ctx.editMessageText("No name")
|
766
|
+
* })
|
767
|
+
*
|
768
|
+
* await ctx.reply("No name (yet)", { reply_markup: menu })
|
769
|
+
* ```
|
770
|
+
*
|
771
|
+
* More information about conversational menus can be found [in the
|
772
|
+
* documentation](https://grammy.dev/plugins/conversations).
|
773
|
+
*
|
774
|
+
* @param id Optional menu identifier
|
775
|
+
* @param options Optional menu options
|
776
|
+
*/
|
777
|
+
menu(id, options) {
|
778
|
+
return this.menuPool.create(id, options);
|
779
|
+
}
|
780
|
+
}
|
781
|
+
function makeAndCombiner(conversation) {
|
782
|
+
return function combineAnd(promise) {
|
783
|
+
const ext = {
|
784
|
+
and(predicate, opts = {}) {
|
785
|
+
const { otherwise, ...skipOptions } = opts;
|
786
|
+
return combineAnd(promise.then(async (ctx) => {
|
787
|
+
if (!await predicate(ctx)) {
|
788
|
+
await (otherwise === null || otherwise === void 0 ? void 0 : otherwise(ctx));
|
789
|
+
await conversation.skip(skipOptions);
|
790
|
+
}
|
791
|
+
return ctx;
|
792
|
+
}));
|
793
|
+
},
|
794
|
+
unless(predicate, opts) {
|
795
|
+
return ext.and(async (ctx) => !await predicate(ctx), opts);
|
796
|
+
},
|
797
|
+
andFor(query, opts) {
|
798
|
+
return ext.and(Context.has.filterQuery(query), opts);
|
799
|
+
},
|
800
|
+
andForHears(trigger, opts) {
|
801
|
+
return ext.and(Context.has.text(trigger), opts);
|
802
|
+
},
|
803
|
+
andForCommand(command, opts) {
|
804
|
+
return ext.and(Context.has.command(command), opts);
|
805
|
+
},
|
806
|
+
andForReaction(reaction, opts) {
|
807
|
+
return ext.and(Context.has.reaction(reaction), opts);
|
808
|
+
},
|
809
|
+
andForCallbackQuery(trigger, opts) {
|
810
|
+
return ext.and(Context.has.callbackQuery(trigger), opts);
|
811
|
+
},
|
812
|
+
andForGameQuery(trigger, opts) {
|
813
|
+
return ext.and(Context.has.gameQuery(trigger), opts);
|
814
|
+
},
|
815
|
+
andFrom(user, opts) {
|
816
|
+
const id = typeof user === "number" ? user : user.id;
|
817
|
+
return ext.and((ctx) => { var _a; return ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) === id; }, opts);
|
818
|
+
},
|
819
|
+
andForReplyTo(message_id, opts) {
|
820
|
+
const id = typeof message_id === "number"
|
821
|
+
? message_id
|
822
|
+
: message_id.message_id;
|
823
|
+
return ext.and((ctx) => {
|
824
|
+
var _a, _b, _c, _d;
|
825
|
+
return ((_b = (_a = ctx.message) === null || _a === void 0 ? void 0 : _a.reply_to_message) === null || _b === void 0 ? void 0 : _b.message_id) === id ||
|
826
|
+
((_d = (_c = ctx.channelPost) === null || _c === void 0 ? void 0 : _c.reply_to_message) === null || _d === void 0 ? void 0 : _d.message_id) === id;
|
827
|
+
}, opts);
|
828
|
+
},
|
829
|
+
};
|
830
|
+
return Object.assign(promise, ext);
|
831
|
+
};
|
832
|
+
}
|