@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.
@@ -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
+ }