@mtcute/dispatcher 0.12.3 → 0.13.0

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.
@@ -26,8 +26,10 @@ export declare class CallbackDataBuilder<T extends string> {
26
26
  * Parse callback data to object
27
27
  *
28
28
  * @param data Callback data as string
29
+ * @param safe If `true`, will return `null` instead of throwing on invalid data
29
30
  */
30
- parse(data: string): Record<T, string>;
31
+ parse(data: string, safe?: false): Record<T, string>;
32
+ parse(data: string, safe: true): Record<T, string> | null;
31
33
  /**
32
34
  * Create a filter for this callback data.
33
35
  *
@@ -43,18 +43,17 @@ class CallbackDataBuilder {
43
43
  }
44
44
  return ret;
45
45
  }
46
- /**
47
- * Parse callback data to object
48
- *
49
- * @param data Callback data as string
50
- */
51
- parse(data) {
46
+ parse(data, safe = false) {
52
47
  const parts = data.split(this.sep);
53
48
  if (parts[0] !== this.prefix) {
54
- throw new core_1.MtArgumentError('Invalid data passed');
49
+ if (safe)
50
+ return null;
51
+ throw new core_1.MtArgumentError(`Invalid data passed: "${data}" (bad prefix, expected ${this.prefix}, got ${parts[0]})`);
55
52
  }
56
53
  if (parts.length !== this._fields.length + 1) {
57
- throw new core_1.MtArgumentError('Invalid data passed');
54
+ if (safe)
55
+ return null;
56
+ throw new core_1.MtArgumentError(`Invalid data passed: "${data}" (bad parts count, expected ${this._fields.length}, got ${parts.length - 1})`);
58
57
  }
59
58
  const ret = {};
60
59
  parts.forEach((it, idx) => {
@@ -82,7 +81,9 @@ class CallbackDataBuilder {
82
81
  return async (query) => {
83
82
  if (!query.dataStr)
84
83
  return false;
85
- const data = this.parse(query.dataStr);
84
+ const data = this.parse(query.dataStr, true);
85
+ if (!data)
86
+ return false;
86
87
  const fnResult = await params(query, data);
87
88
  if (typeof fnResult === 'boolean') {
88
89
  query.match = data;
@@ -1 +1 @@
1
- {"version":3,"file":"callback-data-builder.js","sourceRoot":"","sources":["../../src/callback-data-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallbackQuery, MaybeArray, MaybePromise, MtArgumentError } from '@mtcute/core'\n\nimport { UpdateFilter } from './filters/types.js'\n\n/**\n * Callback data builder, inspired by [aiogram](https://github.com/aiogram/aiogram).\n *\n * This can be used to simplify management of different callbacks.\n *\n * [Learn more in the docs](/guide/topics/keyboards.html#callback-data-builders)\n */\nexport class CallbackDataBuilder<T extends string> {\n private readonly _fields: T[]\n\n sep = ':'\n\n /**\n * @param prefix Prefix for the data. Use something unique across your bot.\n * @param fields Field names in the order they will be serialized.\n */\n constructor(\n public prefix: string,\n ...fields: T[]\n ) {\n this._fields = fields\n }\n\n /**\n * Build a callback data string\n *\n * @param obj Object containing the data\n */\n build(obj: Record<T, string>): string {\n const ret =\n this.prefix +\n this.sep +\n this._fields\n .map((f) => {\n const val = obj[f]\n\n if (val.includes(this.sep)) {\n throw new MtArgumentError(\n `Value for ${f} ${val} contains separator ${this.sep} and cannot be used.`,\n )\n }\n\n return val\n })\n .join(this.sep)\n\n if (ret.length > 64) {\n throw new MtArgumentError('Resulting callback data is too long.')\n }\n\n return ret\n }\n\n /**\n * Parse callback data to object\n *\n * @param data Callback data as string\n */\n parse(data: string): Record<T, string> {\n const parts = data.split(this.sep)\n\n if (parts[0] !== this.prefix) {\n throw new MtArgumentError('Invalid data passed')\n }\n\n if (parts.length !== this._fields.length + 1) {\n throw new MtArgumentError('Invalid data passed')\n }\n\n const ret = {} as Record<T, string>\n parts.forEach((it, idx) => {\n if (idx === 0) return // skip prefix\n\n ret[this._fields[idx - 1]] = it\n })\n\n return ret\n }\n\n /**\n * Create a filter for this callback data.\n *\n * You can either pass an object with field names as keys and values as strings or regexes,\n * which will be compiled to a RegExp, or a function that will be called with the parsed data.\n * Note that the strings will be passed to `RegExp` **directly**, so you may want to escape them.\n *\n * When using a function, you can either return a boolean, or an object with field names as keys\n * and values as strings or regexes. In the latter case, the resulting object will be matched\n * against the parsed data the same way as if you passed it directly.\n *\n * @param params\n */\n filter(\n params:\n | ((\n upd: CallbackQuery,\n parsed: Record<T, string>,\n ) => MaybePromise<Partial<Record<T, MaybeArray<string | RegExp>>> | boolean>)\n | Partial<Record<T, MaybeArray<string | RegExp>>> = {},\n ): UpdateFilter<\n CallbackQuery,\n {\n match: Record<T, string>\n }\n > {\n if (typeof params === 'function') {\n return async (query) => {\n if (!query.dataStr) return false\n\n const data = this.parse(query.dataStr)\n const fnResult = await params(query, data)\n\n if (typeof fnResult === 'boolean') {\n (\n query as CallbackQuery & {\n match: Record<T, string>\n }\n ).match = data\n\n return fnResult\n }\n\n // validate result\n for (const key in fnResult) {\n const value = data[key]\n if (value === undefined) return false\n\n let matchers = fnResult[key] as MaybeArray<string | RegExp>\n if (!Array.isArray(matchers)) matchers = [matchers]\n\n for (const matcher of matchers) {\n if (typeof matcher === 'string') {\n if (value !== matcher) return false\n } else if (!matcher.test(value)) return false\n }\n }\n\n (\n query as CallbackQuery & {\n match: Record<T, string>\n }\n ).match = data\n\n return true\n }\n }\n\n const parts: string[] = []\n\n this._fields.forEach((field) => {\n if (!(field in params)) {\n parts.push(`[^${this.sep}]*?`)\n\n return\n }\n\n const value = params[field]\n\n if (Array.isArray(value)) {\n parts.push(`(${value.map((i) => (typeof i === 'string' ? i : i.source)).join('|')})`)\n } else {\n // noinspection SuspiciousTypeOfGuard\n parts.push(typeof value === 'string' ? value : (value as RegExp).source)\n }\n })\n\n const regex = new RegExp(`^${this.prefix}${this.sep}${parts.join(this.sep)}$`)\n\n return (query) => {\n const m = query.dataStr?.match(regex)\n if (!m) return false\n ;(\n query as CallbackQuery & {\n match: Record<T, string>\n }\n ).match = this.parse(m[0])\n\n return true\n }\n }\n}\n"]}
1
+ {"version":3,"file":"callback-data-builder.js","sourceRoot":"","sources":["../../src/callback-data-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallbackQuery, MaybeArray, MaybePromise, MtArgumentError } from '@mtcute/core'\n\nimport { UpdateFilter } from './filters/types.js'\n\n/**\n * Callback data builder, inspired by [aiogram](https://github.com/aiogram/aiogram).\n *\n * This can be used to simplify management of different callbacks.\n *\n * [Learn more in the docs](/guide/topics/keyboards.html#callback-data-builders)\n */\nexport class CallbackDataBuilder<T extends string> {\n private readonly _fields: T[]\n\n sep = ':'\n\n /**\n * @param prefix Prefix for the data. Use something unique across your bot.\n * @param fields Field names in the order they will be serialized.\n */\n constructor(\n public prefix: string,\n ...fields: T[]\n ) {\n this._fields = fields\n }\n\n /**\n * Build a callback data string\n *\n * @param obj Object containing the data\n */\n build(obj: Record<T, string>): string {\n const ret =\n this.prefix +\n this.sep +\n this._fields\n .map((f) => {\n const val = obj[f]\n\n if (val.includes(this.sep)) {\n throw new MtArgumentError(\n `Value for ${f} ${val} contains separator ${this.sep} and cannot be used.`,\n )\n }\n\n return val\n })\n .join(this.sep)\n\n if (ret.length > 64) {\n throw new MtArgumentError('Resulting callback data is too long.')\n }\n\n return ret\n }\n\n /**\n * Parse callback data to object\n *\n * @param data Callback data as string\n * @param safe If `true`, will return `null` instead of throwing on invalid data\n */\n parse(data: string, safe?: false): Record<T, string>\n parse(data: string, safe: true): Record<T, string> | null\n parse(data: string, safe = false): Record<T, string> | null {\n const parts = data.split(this.sep)\n\n if (parts[0] !== this.prefix) {\n if (safe) return null\n throw new MtArgumentError(\n `Invalid data passed: \"${data}\" (bad prefix, expected ${this.prefix}, got ${parts[0]})`,\n )\n }\n\n if (parts.length !== this._fields.length + 1) {\n if (safe) return null\n throw new MtArgumentError(\n `Invalid data passed: \"${data}\" (bad parts count, expected ${this._fields.length}, got ${\n parts.length - 1\n })`,\n )\n }\n\n const ret = {} as Record<T, string>\n parts.forEach((it, idx) => {\n if (idx === 0) return // skip prefix\n\n ret[this._fields[idx - 1]] = it\n })\n\n return ret\n }\n\n /**\n * Create a filter for this callback data.\n *\n * You can either pass an object with field names as keys and values as strings or regexes,\n * which will be compiled to a RegExp, or a function that will be called with the parsed data.\n * Note that the strings will be passed to `RegExp` **directly**, so you may want to escape them.\n *\n * When using a function, you can either return a boolean, or an object with field names as keys\n * and values as strings or regexes. In the latter case, the resulting object will be matched\n * against the parsed data the same way as if you passed it directly.\n *\n * @param params\n */\n filter(\n params:\n | ((\n upd: CallbackQuery,\n parsed: Record<T, string>,\n ) => MaybePromise<Partial<Record<T, MaybeArray<string | RegExp>>> | boolean>)\n | Partial<Record<T, MaybeArray<string | RegExp>>> = {},\n ): UpdateFilter<\n CallbackQuery,\n {\n match: Record<T, string>\n }\n > {\n if (typeof params === 'function') {\n return async (query) => {\n if (!query.dataStr) return false\n\n const data = this.parse(query.dataStr, true)\n if (!data) return false\n\n const fnResult = await params(query, data)\n\n if (typeof fnResult === 'boolean') {\n (\n query as CallbackQuery & {\n match: Record<T, string>\n }\n ).match = data\n\n return fnResult\n }\n\n // validate result\n for (const key in fnResult) {\n const value = data[key]\n if (value === undefined) return false\n\n let matchers = fnResult[key] as MaybeArray<string | RegExp>\n if (!Array.isArray(matchers)) matchers = [matchers]\n\n for (const matcher of matchers) {\n if (typeof matcher === 'string') {\n if (value !== matcher) return false\n } else if (!matcher.test(value)) return false\n }\n }\n\n (\n query as CallbackQuery & {\n match: Record<T, string>\n }\n ).match = data\n\n return true\n }\n }\n\n const parts: string[] = []\n\n this._fields.forEach((field) => {\n if (!(field in params)) {\n parts.push(`[^${this.sep}]*?`)\n\n return\n }\n\n const value = params[field]\n\n if (Array.isArray(value)) {\n parts.push(`(${value.map((i) => (typeof i === 'string' ? i : i.source)).join('|')})`)\n } else {\n // noinspection SuspiciousTypeOfGuard\n parts.push(typeof value === 'string' ? value : (value as RegExp).source)\n }\n })\n\n const regex = new RegExp(`^${this.prefix}${this.sep}${parts.join(this.sep)}$`)\n\n return (query) => {\n const m = query.dataStr?.match(regex)\n if (!m) return false\n ;(\n query as CallbackQuery & {\n match: Record<T, string>\n }\n ).match = this.parse(m[0])\n\n return true\n }\n }\n}\n"]}
@@ -0,0 +1,24 @@
1
+ import { BusinessMessageContext } from './business-message.js';
2
+ import { CallbackQueryContext, InlineCallbackQueryContext } from './callback-query.js';
3
+ import { MessageContext } from './message.js';
4
+ import { UpdateContextType } from './parse.js';
5
+ /** Update which is dispatched whenever scene is entered or exited */
6
+ export declare class SceneTransitionContext {
7
+ /** Name of the previous scene, if any */
8
+ readonly previousScene: string | null;
9
+ /** Update, handler for which triggered the transition */
10
+ readonly update: UpdateContextType;
11
+ constructor(
12
+ /** Name of the previous scene, if any */
13
+ previousScene: string | null,
14
+ /** Update, handler for which triggered the transition */
15
+ update: UpdateContextType);
16
+ /** Get {@link update}, asserting it is a message-related update */
17
+ get message(): MessageContext;
18
+ /** Get {@link update}, asserting it is a business message-related update */
19
+ get businessMessage(): BusinessMessageContext;
20
+ /** Get {@link update}, asserting it is a callback query update */
21
+ get callbackQuery(): CallbackQueryContext;
22
+ /** Get {@link update}, asserting it is an inline callback query update */
23
+ get inlineCallbackQuery(): InlineCallbackQueryContext;
24
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SceneTransitionContext = void 0;
4
+ const core_1 = require("@mtcute/core");
5
+ const utils_js_1 = require("@mtcute/core/utils.js");
6
+ const business_message_js_1 = require("./business-message.js");
7
+ const callback_query_js_1 = require("./callback-query.js");
8
+ const message_js_1 = require("./message.js");
9
+ /** Update which is dispatched whenever scene is entered or exited */
10
+ class SceneTransitionContext {
11
+ previousScene;
12
+ update;
13
+ constructor(
14
+ /** Name of the previous scene, if any */
15
+ previousScene,
16
+ /** Update, handler for which triggered the transition */
17
+ update) {
18
+ this.previousScene = previousScene;
19
+ this.update = update;
20
+ }
21
+ /** Get {@link update}, asserting it is a message-related update */
22
+ get message() {
23
+ if (this.update instanceof message_js_1.MessageContext) {
24
+ return this.update;
25
+ }
26
+ throw new core_1.MtTypeAssertionError('SceneTransitionContext.message', 'message', this.update._name);
27
+ }
28
+ /** Get {@link update}, asserting it is a business message-related update */
29
+ get businessMessage() {
30
+ if (this.update instanceof business_message_js_1.BusinessMessageContext) {
31
+ return this.update;
32
+ }
33
+ throw new core_1.MtTypeAssertionError('SceneTransitionContext.businessMessage', 'business message', this.update._name);
34
+ }
35
+ /** Get {@link update}, asserting it is a callback query update */
36
+ get callbackQuery() {
37
+ if (this.update instanceof callback_query_js_1.CallbackQueryContext) {
38
+ return this.update;
39
+ }
40
+ throw new core_1.MtTypeAssertionError('SceneTransitionContext.callbackQuery', 'callback query', this.update._name);
41
+ }
42
+ /** Get {@link update}, asserting it is an inline callback query update */
43
+ get inlineCallbackQuery() {
44
+ if (this.update instanceof callback_query_js_1.InlineCallbackQueryContext) {
45
+ return this.update;
46
+ }
47
+ throw new core_1.MtTypeAssertionError('SceneTransitionContext.inlineCallbackQuery', 'inline callback query', this.update._name);
48
+ }
49
+ }
50
+ exports.SceneTransitionContext = SceneTransitionContext;
51
+ (0, utils_js_1.makeInspectable)(SceneTransitionContext);
52
+ //# sourceMappingURL=scene-transition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-transition.js","sourceRoot":"","sources":["../../../src/context/scene-transition.ts"],"names":[],"mappings":";;;AAAA,uCAAmD;AACnD,oDAAuD;AAEvD,+DAA8D;AAC9D,2DAAsF;AACtF,6CAA6C;AAG7C,qEAAqE;AACrE,MAAa,sBAAsB;IAGlB;IAEA;IAJb;IACI,yCAAyC;IAChC,aAA4B;IACrC,yDAAyD;IAChD,MAAyB;QAFzB,kBAAa,GAAb,aAAa,CAAe;QAE5B,WAAM,GAAN,MAAM,CAAmB;IACnC,CAAC;IAEJ,mEAAmE;IACnE,IAAI,OAAO;QACP,IAAI,IAAI,CAAC,MAAM,YAAY,2BAAc,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC,MAAM,CAAA;QACtB,CAAC;QAED,MAAM,IAAI,2BAAoB,CAAC,gCAAgC,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAClG,CAAC;IAED,4EAA4E;IAC5E,IAAI,eAAe;QACf,IAAI,IAAI,CAAC,MAAM,YAAY,4CAAsB,EAAE,CAAC;YAChD,OAAO,IAAI,CAAC,MAAM,CAAA;QACtB,CAAC;QAED,MAAM,IAAI,2BAAoB,CAAC,wCAAwC,EAAE,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACnH,CAAC;IAED,kEAAkE;IAClE,IAAI,aAAa;QACb,IAAI,IAAI,CAAC,MAAM,YAAY,wCAAoB,EAAE,CAAC;YAC9C,OAAO,IAAI,CAAC,MAAM,CAAA;QACtB,CAAC;QAED,MAAM,IAAI,2BAAoB,CAAC,sCAAsC,EAAE,gBAAgB,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC/G,CAAC;IAED,0EAA0E;IAC1E,IAAI,mBAAmB;QACnB,IAAI,IAAI,CAAC,MAAM,YAAY,8CAA0B,EAAE,CAAC;YACpD,OAAO,IAAI,CAAC,MAAM,CAAA;QACtB,CAAC;QAED,MAAM,IAAI,2BAAoB,CAC1B,4CAA4C,EAC5C,uBAAuB,EACvB,IAAI,CAAC,MAAM,CAAC,KAAK,CACpB,CAAA;IACL,CAAC;CACJ;AA/CD,wDA+CC;AAED,IAAA,0BAAe,EAAC,sBAAsB,CAAC,CAAA","sourcesContent":["import { MtTypeAssertionError } from '@mtcute/core'\nimport { makeInspectable } from '@mtcute/core/utils.js'\n\nimport { BusinessMessageContext } from './business-message.js'\nimport { CallbackQueryContext, InlineCallbackQueryContext } from './callback-query.js'\nimport { MessageContext } from './message.js'\nimport { UpdateContextType } from './parse.js'\n\n/** Update which is dispatched whenever scene is entered or exited */\nexport class SceneTransitionContext {\n constructor(\n /** Name of the previous scene, if any */\n readonly previousScene: string | null,\n /** Update, handler for which triggered the transition */\n readonly update: UpdateContextType,\n ) {}\n\n /** Get {@link update}, asserting it is a message-related update */\n get message(): MessageContext {\n if (this.update instanceof MessageContext) {\n return this.update\n }\n\n throw new MtTypeAssertionError('SceneTransitionContext.message', 'message', this.update._name)\n }\n\n /** Get {@link update}, asserting it is a business message-related update */\n get businessMessage(): BusinessMessageContext {\n if (this.update instanceof BusinessMessageContext) {\n return this.update\n }\n\n throw new MtTypeAssertionError('SceneTransitionContext.businessMessage', 'business message', this.update._name)\n }\n\n /** Get {@link update}, asserting it is a callback query update */\n get callbackQuery(): CallbackQueryContext {\n if (this.update instanceof CallbackQueryContext) {\n return this.update\n }\n\n throw new MtTypeAssertionError('SceneTransitionContext.callbackQuery', 'callback query', this.update._name)\n }\n\n /** Get {@link update}, asserting it is an inline callback query update */\n get inlineCallbackQuery(): InlineCallbackQueryContext {\n if (this.update instanceof InlineCallbackQueryContext) {\n return this.update\n }\n\n throw new MtTypeAssertionError(\n 'SceneTransitionContext.inlineCallbackQuery',\n 'inline callback query',\n this.update._name,\n )\n }\n}\n\nmakeInspectable(SceneTransitionContext)\n"]}
@@ -3,6 +3,7 @@ import { TelegramClient } from '@mtcute/core/client.js';
3
3
  import { UpdateContext } from './context/base.js';
4
4
  import { BusinessMessageContext } from './context/business-message.js';
5
5
  import { CallbackQueryContext, ChatJoinRequestUpdateContext, ChosenInlineResultContext, InlineCallbackQueryContext, InlineQueryContext, MessageContext, PreCheckoutQueryContext } from './context/index.js';
6
+ import { SceneTransitionContext } from './context/scene-transition.js';
6
7
  import { filters, UpdateFilter } from './filters/index.js';
7
8
  import { BotChatJoinRequestHandler, BotReactionCountUpdateHandler, BotReactionUpdateHandler, BotStoppedHandler, BusinessConnectionUpdateHandler, BusinessMessageGroupHandler, CallbackQueryHandler, ChatJoinRequestHandler, ChatMemberUpdateHandler, ChosenInlineResultHandler, DeleteBusinessMessageHandler, DeleteMessageHandler, DeleteStoryHandler, EditBusinessMessageHandler, EditMessageHandler, HistoryReadHandler, InlineCallbackQueryHandler, InlineQueryHandler, MessageGroupHandler, NewBusinessMessageHandler, NewMessageHandler, PollUpdateHandler, PollVoteHandler, PreCheckoutQueryHandler, RawUpdateHandler, StoryUpdateHandler, UpdateHandler, UserStatusUpdateHandler, UserTypingHandler } from './handler.js';
8
9
  import { PropagationAction } from './propagation.js';
@@ -47,11 +48,14 @@ export declare class Dispatcher<State extends object = never> {
47
48
  private _errorHandler?;
48
49
  private _preUpdateHandler?;
49
50
  private _postUpdateHandler?;
51
+ private _sceneTransitionHandler?;
50
52
  protected constructor(client?: TelegramClient, params?: DispatcherParams);
51
53
  /**
52
54
  * Create a new dispatcher and bind it to the client.
53
55
  */
54
- static for<State extends object = never>(client: TelegramClient, params?: DispatcherParams): Dispatcher<State>;
56
+ static for<State extends object = never>(client: TelegramClient, ...args: [State] extends [never] ? [params?: DispatcherParams] : [params: DispatcherParams & {
57
+ storage: IStateStorageProvider;
58
+ }]): Dispatcher<State>;
55
59
  /**
56
60
  * Create a new child dispatcher.
57
61
  */
@@ -340,6 +344,24 @@ export declare class Dispatcher<State extends object = never> {
340
344
  * @param group Handler group index
341
345
  */
342
346
  onRawUpdate(filter: RawUpdateHandler['check'], handler: RawUpdateHandler['callback'], group?: number): void;
347
+ /**
348
+ * Register a scene transition handler
349
+ *
350
+ * This handler is called whenever a scene transition occurs
351
+ * in the context of the scene that is being entered,
352
+ * and before any of the its own handlers are called,
353
+ * and can be used to customize the transition behavior:
354
+ * - `Stop` to prevent dispatching the update any further **even if ToScene/ToRoot was used**
355
+ * - `Continue` same as Stop, but still dispatch the update to children
356
+ * - `ToScene` to prevent the transition and dispatch the update to the scene entered in the transition handler
357
+ *
358
+ * > **Note**: if multiple `state.enter()` calls were made within the same update,
359
+ * > this handler will only be called for the last one.
360
+ *
361
+ * @param handler Raw update handler
362
+ * @param group Handler group index
363
+ */
364
+ onSceneTransition(handler: ((ctx: SceneTransitionContext, state: UpdateState<State>) => MaybePromise<PropagationAction | void>) | null): void;
343
365
  /**
344
366
  * Register a new message handler without any filters
345
367
  *
package/cjs/dispatcher.js CHANGED
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.Dispatcher = void 0;
8
8
  const core_1 = require("@mtcute/core");
9
9
  const parse_js_1 = require("./context/parse.js");
10
+ const scene_transition_js_1 = require("./context/scene-transition.js");
10
11
  const index_js_1 = require("./state/index.js");
11
12
  const service_js_1 = require("./state/service.js");
12
13
  /**
@@ -29,6 +30,7 @@ class Dispatcher {
29
30
  _errorHandler;
30
31
  _preUpdateHandler;
31
32
  _postUpdateHandler;
33
+ _sceneTransitionHandler;
32
34
  constructor(client, params) {
33
35
  this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this);
34
36
  this.dispatchUpdate = this.dispatchUpdate.bind(this);
@@ -57,9 +59,6 @@ class Dispatcher {
57
59
  }
58
60
  }
59
61
  }
60
- /**
61
- * Create a new dispatcher and bind it to the client.
62
- */
63
62
  static for(client, params) {
64
63
  return new Dispatcher(client, params);
65
64
  }
@@ -245,7 +244,10 @@ class Dispatcher {
245
244
  (update.name === 'new_message' ||
246
245
  update.name === 'edit_message' ||
247
246
  update.name === 'callback_query' ||
248
- update.name === 'message_group')) {
247
+ update.name === 'message_group' ||
248
+ update.name === 'new_business_message' ||
249
+ update.name === 'edit_business_message' ||
250
+ update.name === 'business_message_group')) {
249
251
  // no need to fetch scene if there are no registered scenes
250
252
  if (!parsedContext)
251
253
  parsedContext = (0, parse_js_1._parsedUpdateToContext)(this._client, update);
@@ -281,7 +283,10 @@ class Dispatcher {
281
283
  (update.name === 'new_message' ||
282
284
  update.name === 'edit_message' ||
283
285
  update.name === 'callback_query' ||
284
- update.name === 'message_group')) {
286
+ update.name === 'message_group' ||
287
+ update.name === 'new_business_message' ||
288
+ update.name === 'edit_business_message' ||
289
+ update.name === 'business_message_group')) {
285
290
  if (!parsedContext)
286
291
  parsedContext = (0, parse_js_1._parsedUpdateToContext)(this._client, update);
287
292
  const key = await this._stateKeyDelegate(parsedContext);
@@ -327,6 +332,31 @@ class Dispatcher {
327
332
  }
328
333
  else
329
334
  continue;
335
+ if (parsedState && this._scenes) {
336
+ // check if scene transition was made
337
+ const newScene = parsedState.scene;
338
+ if (parsedScene !== newScene) {
339
+ const nextDp = newScene ? this._scenes.get(newScene) : this._parent;
340
+ if (!nextDp) {
341
+ throw new core_1.MtArgumentError(`Scene ${newScene} not found`);
342
+ }
343
+ if (nextDp._sceneTransitionHandler) {
344
+ const transition = new scene_transition_js_1.SceneTransitionContext(parsedScene, parsedContext);
345
+ const transitionResult = await nextDp._sceneTransitionHandler?.(transition, parsedState);
346
+ switch (transitionResult) {
347
+ case 'stop':
348
+ return true;
349
+ case 'continue':
350
+ continue;
351
+ case 'scene': {
352
+ const scene = parsedState.scene;
353
+ const dp = scene ? nextDp._scenes.get(scene) : nextDp._parent;
354
+ return dp._dispatchUpdateNowImpl(update, undefined, scene, true);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
330
360
  switch (result) {
331
361
  case 'continue':
332
362
  continue;
@@ -339,11 +369,9 @@ class Dispatcher {
339
369
  if (!parsedState) {
340
370
  throw new core_1.MtArgumentError('Cannot use ToScene without state');
341
371
  }
342
- const scene = parsedState['_scene'];
343
- if (!scene) {
344
- throw new core_1.MtArgumentError('Cannot use ToScene without entering a scene');
345
- }
346
- return this._scenes.get(scene)._dispatchUpdateNowImpl(update, undefined, scene, true);
372
+ const scene = parsedState.scene;
373
+ const dp = scene ? this._scenes.get(scene) : this._parent;
374
+ return dp._dispatchUpdateNowImpl(update, undefined, scene, true);
347
375
  }
348
376
  }
349
377
  break;
@@ -513,6 +541,7 @@ class Dispatcher {
513
541
  child._client = this._client;
514
542
  child._storage = this._storage;
515
543
  child._deps = this._deps;
544
+ child._scenes = this._scenes;
516
545
  child._stateKeyDelegate = this._stateKeyDelegate;
517
546
  child._customStorage ??= this._customStorage;
518
547
  child._customStateKeyDelegate ??= this._customStateKeyDelegate;
@@ -730,6 +759,29 @@ class Dispatcher {
730
759
  onRawUpdate(filter, handler, group) {
731
760
  this._addKnownHandler('raw', filter, handler, group);
732
761
  }
762
+ /**
763
+ * Register a scene transition handler
764
+ *
765
+ * This handler is called whenever a scene transition occurs
766
+ * in the context of the scene that is being entered,
767
+ * and before any of the its own handlers are called,
768
+ * and can be used to customize the transition behavior:
769
+ * - `Stop` to prevent dispatching the update any further **even if ToScene/ToRoot was used**
770
+ * - `Continue` same as Stop, but still dispatch the update to children
771
+ * - `ToScene` to prevent the transition and dispatch the update to the scene entered in the transition handler
772
+ *
773
+ * > **Note**: if multiple `state.enter()` calls were made within the same update,
774
+ * > this handler will only be called for the last one.
775
+ *
776
+ * @param handler Raw update handler
777
+ * @param group Handler group index
778
+ */
779
+ onSceneTransition(handler) {
780
+ if (handler)
781
+ this._sceneTransitionHandler = handler;
782
+ else
783
+ this._sceneTransitionHandler = undefined;
784
+ }
733
785
  /** @internal */
734
786
  onNewMessage(filter, handler, group) {
735
787
  this._addKnownHandler('new_message', filter, handler, group);