telegram-botbuilder 1.6.6 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/Changelog.md +81 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.md +453 -0
  4. package/README.md +403 -0
  5. package/lib/actions/callback_action.d.ts +14 -0
  6. package/lib/actions/callback_action.d.ts.map +1 -0
  7. package/lib/actions/callback_action.js +25 -0
  8. package/lib/actions/callback_action.js.map +1 -0
  9. package/lib/actions/change_dialog.d.ts +10 -0
  10. package/lib/actions/change_dialog.d.ts.map +1 -0
  11. package/lib/actions/change_dialog.js +17 -0
  12. package/lib/actions/change_dialog.js.map +1 -0
  13. package/lib/actions/control_flow.d.ts +26 -0
  14. package/lib/actions/control_flow.d.ts.map +1 -0
  15. package/lib/actions/control_flow.js +61 -0
  16. package/lib/actions/control_flow.js.map +1 -0
  17. package/lib/actions/index.d.ts +6 -0
  18. package/lib/actions/index.d.ts.map +1 -0
  19. package/lib/actions/index.js +11 -0
  20. package/lib/actions/index.js.map +1 -0
  21. package/lib/actions/send_message.d.ts +16 -0
  22. package/lib/actions/send_message.d.ts.map +1 -0
  23. package/lib/actions/send_message.js +32 -0
  24. package/lib/actions/send_message.js.map +1 -0
  25. package/lib/actions/wait_for_input.d.ts +14 -0
  26. package/lib/actions/wait_for_input.d.ts.map +1 -0
  27. package/lib/actions/wait_for_input.js +54 -0
  28. package/lib/actions/wait_for_input.js.map +1 -0
  29. package/lib/core/bot_builder.d.ts +145 -0
  30. package/lib/core/bot_builder.d.ts.map +1 -0
  31. package/lib/core/bot_builder.js +689 -0
  32. package/lib/core/bot_builder.js.map +1 -0
  33. package/lib/core/button_registry.d.ts +39 -0
  34. package/lib/core/button_registry.d.ts.map +1 -0
  35. package/lib/core/button_registry.js +84 -0
  36. package/lib/core/button_registry.js.map +1 -0
  37. package/lib/core/dialog_manager.d.ts +71 -0
  38. package/lib/core/dialog_manager.d.ts.map +1 -0
  39. package/lib/core/dialog_manager.js +146 -0
  40. package/lib/core/dialog_manager.js.map +1 -0
  41. package/lib/core/index.d.ts +8 -0
  42. package/lib/core/index.d.ts.map +1 -0
  43. package/lib/core/index.js +8 -0
  44. package/lib/core/index.js.map +1 -0
  45. package/lib/core/input_manager.d.ts +49 -0
  46. package/lib/core/input_manager.d.ts.map +1 -0
  47. package/lib/core/input_manager.js +129 -0
  48. package/lib/core/input_manager.js.map +1 -0
  49. package/lib/core/keyboard_builder.d.ts +35 -0
  50. package/lib/core/keyboard_builder.d.ts.map +1 -0
  51. package/lib/core/keyboard_builder.js +87 -0
  52. package/lib/core/keyboard_builder.js.map +1 -0
  53. package/lib/core/middleware_chain.d.ts +25 -0
  54. package/lib/core/middleware_chain.d.ts.map +1 -0
  55. package/lib/core/middleware_chain.js +54 -0
  56. package/lib/core/middleware_chain.js.map +1 -0
  57. package/lib/core/schema_compiler.d.ts +25 -0
  58. package/lib/core/schema_compiler.d.ts.map +1 -0
  59. package/lib/core/schema_compiler.js +65 -0
  60. package/lib/core/schema_compiler.js.map +1 -0
  61. package/lib/errors/bot_error.d.ts +7 -0
  62. package/lib/errors/bot_error.d.ts.map +1 -0
  63. package/lib/errors/bot_error.js +17 -0
  64. package/lib/errors/bot_error.js.map +1 -0
  65. package/lib/errors/dialog_error.d.ts +12 -0
  66. package/lib/errors/dialog_error.d.ts.map +1 -0
  67. package/lib/errors/dialog_error.js +22 -0
  68. package/lib/errors/dialog_error.js.map +1 -0
  69. package/lib/errors/index.d.ts +5 -0
  70. package/lib/errors/index.d.ts.map +1 -0
  71. package/lib/errors/index.js +5 -0
  72. package/lib/errors/index.js.map +1 -0
  73. package/lib/errors/telegram_error.d.ts +12 -0
  74. package/lib/errors/telegram_error.d.ts.map +1 -0
  75. package/lib/errors/telegram_error.js +39 -0
  76. package/lib/errors/telegram_error.js.map +1 -0
  77. package/lib/errors/validation_error.d.ts +19 -0
  78. package/lib/errors/validation_error.d.ts.map +1 -0
  79. package/lib/errors/validation_error.js +35 -0
  80. package/lib/errors/validation_error.js.map +1 -0
  81. package/lib/index.d.ts +6 -2
  82. package/lib/index.d.ts.map +1 -1
  83. package/lib/index.js +9 -18
  84. package/lib/index.js.map +1 -1
  85. package/lib/types/action.d.ts +58 -0
  86. package/lib/types/action.d.ts.map +1 -0
  87. package/lib/types/action.js +2 -0
  88. package/lib/types/action.js.map +1 -0
  89. package/lib/types/config.d.ts +36 -0
  90. package/lib/types/config.d.ts.map +1 -0
  91. package/lib/types/config.js +12 -0
  92. package/lib/types/config.js.map +1 -0
  93. package/lib/types/dialog.d.ts +39 -0
  94. package/lib/types/dialog.d.ts.map +1 -0
  95. package/lib/types/dialog.js +2 -0
  96. package/lib/types/dialog.js.map +1 -0
  97. package/lib/types/index.d.ts +10 -0
  98. package/lib/types/index.d.ts.map +1 -0
  99. package/lib/types/index.js +3 -0
  100. package/lib/types/index.js.map +1 -0
  101. package/lib/types/internal.d.ts +58 -0
  102. package/lib/types/internal.d.ts.map +1 -0
  103. package/lib/types/internal.js +11 -0
  104. package/lib/types/internal.js.map +1 -0
  105. package/lib/types/keyboard.d.ts +41 -0
  106. package/lib/types/keyboard.d.ts.map +1 -0
  107. package/lib/types/keyboard.js +2 -0
  108. package/lib/types/keyboard.js.map +1 -0
  109. package/lib/types/middleware.d.ts +24 -0
  110. package/lib/types/middleware.d.ts.map +1 -0
  111. package/lib/types/middleware.js +2 -0
  112. package/lib/types/middleware.js.map +1 -0
  113. package/lib/types/schema.d.ts +17 -0
  114. package/lib/types/schema.d.ts.map +1 -0
  115. package/lib/types/schema.js +2 -0
  116. package/lib/types/schema.js.map +1 -0
  117. package/lib/utils/constants.d.ts +33 -0
  118. package/lib/utils/constants.d.ts.map +1 -0
  119. package/lib/utils/constants.js +33 -0
  120. package/lib/utils/constants.js.map +1 -0
  121. package/lib/utils/deep_copy.d.ts +6 -0
  122. package/lib/utils/deep_copy.d.ts.map +1 -0
  123. package/lib/utils/deep_copy.js +45 -0
  124. package/lib/utils/deep_copy.js.map +1 -0
  125. package/lib/utils/hash.d.ts +17 -0
  126. package/lib/utils/hash.d.ts.map +1 -0
  127. package/lib/utils/hash.js +50 -0
  128. package/lib/utils/hash.js.map +1 -0
  129. package/lib/utils/index.d.ts +7 -0
  130. package/lib/utils/index.d.ts.map +1 -0
  131. package/lib/utils/index.js +7 -0
  132. package/lib/utils/index.js.map +1 -0
  133. package/lib/utils/logger.d.ts +17 -0
  134. package/lib/utils/logger.d.ts.map +1 -0
  135. package/lib/utils/logger.js +91 -0
  136. package/lib/utils/logger.js.map +1 -0
  137. package/lib/utils/resolvers.d.ts +29 -0
  138. package/lib/utils/resolvers.d.ts.map +1 -0
  139. package/lib/utils/resolvers.js +60 -0
  140. package/lib/utils/resolvers.js.map +1 -0
  141. package/lib/utils/type_guards.d.ts +22 -0
  142. package/lib/utils/type_guards.d.ts.map +1 -0
  143. package/lib/utils/type_guards.js +38 -0
  144. package/lib/utils/type_guards.js.map +1 -0
  145. package/lib/validation/index.d.ts +2 -0
  146. package/lib/validation/index.d.ts.map +1 -0
  147. package/lib/validation/index.js +2 -0
  148. package/lib/validation/index.js.map +1 -0
  149. package/lib/validation/schema_validator.d.ts +11 -0
  150. package/lib/validation/schema_validator.d.ts.map +1 -0
  151. package/lib/validation/schema_validator.js +156 -0
  152. package/lib/validation/schema_validator.js.map +1 -0
  153. package/package.json +65 -21
  154. package/lib/bot-service.d.ts +0 -27
  155. package/lib/bot-service.d.ts.map +0 -1
  156. package/lib/bot-service.js +0 -326
  157. package/lib/bot-service.js.map +0 -1
  158. package/lib/bot-struct.d.ts +0 -58
  159. package/lib/bot-struct.d.ts.map +0 -1
  160. package/lib/bot-struct.js +0 -3
  161. package/lib/bot-struct.js.map +0 -1
  162. package/readme.md +0 -54
  163. package/src/bot-service.ts +0 -289
  164. package/src/bot-struct.ts +0 -59
  165. package/src/index.ts +0 -2
  166. package/tsconfig.json +0 -108
@@ -0,0 +1,689 @@
1
+ import TelegramBot from "node-telegram-bot-api";
2
+ import { EventEmitter } from "node:events";
3
+ import { DEFAULT_CONFIG } from "../types/config.js";
4
+ import { Logger } from "../utils/logger.js";
5
+ import { resolve_text, resolve_images, resolve_buttons } from "../utils/resolvers.js";
6
+ import { has_text, has_document, has_photo, has_contact, has_location } from "../utils/type_guards.js";
7
+ import { DialogManager } from "./dialog_manager.js";
8
+ import { ButtonRegistry } from "./button_registry.js";
9
+ import { KeyboardBuilder } from "./keyboard_builder.js";
10
+ import { MiddlewareChain } from "./middleware_chain.js";
11
+ import { InputManager } from "./input_manager.js";
12
+ import { SchemaCompiler } from "./schema_compiler.js";
13
+ import { TelegramError } from "../errors/telegram_error.js";
14
+ import { DialogNotFoundError } from "../errors/dialog_error.js";
15
+ import { readFile, unlink } from "node:fs/promises";
16
+ export class BotBuilder {
17
+ /** Raw telegram bot instance (for advanced usage) */
18
+ telegram;
19
+ /** Event emitter for custom events */
20
+ events;
21
+ config;
22
+ logger;
23
+ schema;
24
+ dialog_manager;
25
+ button_registry;
26
+ keyboard_builder;
27
+ middleware_chain;
28
+ input_manager;
29
+ constructor(schema, config) {
30
+ // Merge config with defaults
31
+ this.config = {
32
+ ...DEFAULT_CONFIG,
33
+ ...config,
34
+ logger: {
35
+ ...DEFAULT_CONFIG.logger,
36
+ ...config.logger,
37
+ },
38
+ };
39
+ // Initialize logger
40
+ this.logger = new Logger(this.config.logger);
41
+ this.logger.info("Initializing BotBuilder...");
42
+ // Compile schema
43
+ const compiler = new SchemaCompiler(this.logger);
44
+ this.schema = compiler.compile(schema, this.config.validate_schema);
45
+ // Initialize components
46
+ this.dialog_manager = new DialogManager(this.schema.start_dialog);
47
+ this.button_registry = new ButtonRegistry();
48
+ this.keyboard_builder = new KeyboardBuilder(this.button_registry);
49
+ this.middleware_chain = new MiddlewareChain(this.logger);
50
+ this.input_manager = new InputManager(this.logger, this.config.default_input_timeout);
51
+ this.events = new EventEmitter();
52
+ // Initialize Telegram bot
53
+ this.telegram = new TelegramBot(config.token, config.telegram_options);
54
+ // Set up handlers
55
+ this.setup_handlers();
56
+ this.logger.info("BotBuilder initialized successfully");
57
+ }
58
+ // ==================== PUBLIC API ====================
59
+ /**
60
+ * Add middleware to the processing chain
61
+ */
62
+ use(middleware) {
63
+ this.middleware_chain.use(middleware);
64
+ return this;
65
+ }
66
+ /**
67
+ * Navigate user to a specific dialog
68
+ */
69
+ async change_dialog(chat_id, dialog_id) {
70
+ const dialog = this.schema.dialogs.get(dialog_id);
71
+ if (!dialog) {
72
+ throw new DialogNotFoundError(dialog_id, chat_id);
73
+ }
74
+ const state = this.dialog_manager.get_state(chat_id);
75
+ const previous_dialog_id = state.current_dialog_id;
76
+ // Cancel any pending input
77
+ if (state.waiting_for_input) {
78
+ this.input_manager.cancel_wait(state.input_wait_id);
79
+ this.dialog_manager.clear_waiting(chat_id);
80
+ }
81
+ // Call on_leave for previous dialog
82
+ const previous_dialog = this.schema.dialogs.get(previous_dialog_id);
83
+ if (previous_dialog?.on_leave) {
84
+ await previous_dialog.on_leave({
85
+ chat_id,
86
+ bot: this,
87
+ other_dialog_id: dialog_id,
88
+ });
89
+ }
90
+ // Update state
91
+ this.dialog_manager.set_dialog(chat_id, dialog_id);
92
+ // Call on_enter for new dialog
93
+ if (dialog.on_enter) {
94
+ await dialog.on_enter({
95
+ chat_id,
96
+ bot: this,
97
+ other_dialog_id: previous_dialog_id,
98
+ });
99
+ }
100
+ // Render the dialog
101
+ await this.render_dialog(chat_id, dialog);
102
+ }
103
+ /**
104
+ * Send a standalone message (not part of dialog system)
105
+ */
106
+ async send_message(chat_id, text, options) {
107
+ return this.telegram.sendMessage(chat_id, text, {
108
+ parse_mode: options?.parse_mode ?? this.config.default_parse_mode,
109
+ disable_web_page_preview: options?.disable_web_page_preview,
110
+ disable_notification: options?.disable_notification,
111
+ protect_content: options?.protect_content,
112
+ });
113
+ }
114
+ /**
115
+ * Get current user state
116
+ */
117
+ get_user_state(chat_id) {
118
+ return this.dialog_manager.get_state(chat_id);
119
+ }
120
+ /**
121
+ * Set custom data in user state
122
+ */
123
+ set_user_data(chat_id, key, value) {
124
+ this.dialog_manager.set_custom_data(chat_id, key, value);
125
+ }
126
+ /**
127
+ * Get custom data from user state
128
+ */
129
+ get_user_data(chat_id, key) {
130
+ return this.dialog_manager.get_custom_data(chat_id, key);
131
+ }
132
+ /**
133
+ * Reset user state to start dialog
134
+ */
135
+ async reset_user(chat_id) {
136
+ this.dialog_manager.reset(chat_id);
137
+ await this.change_dialog(chat_id, this.schema.start_dialog);
138
+ }
139
+ /**
140
+ * Delete a message
141
+ */
142
+ async delete_message(chat_id, message_id) {
143
+ try {
144
+ await this.telegram.deleteMessage(chat_id, message_id);
145
+ return true;
146
+ }
147
+ catch (error) {
148
+ const tg_error = TelegramError.from_error(error, chat_id);
149
+ if (tg_error.is_message_not_found()) {
150
+ return false;
151
+ }
152
+ throw tg_error;
153
+ }
154
+ }
155
+ /**
156
+ * Get a dialog by ID
157
+ */
158
+ get_dialog(dialog_id) {
159
+ return this.schema.dialogs.get(dialog_id);
160
+ }
161
+ /**
162
+ * Wait for user text input
163
+ */
164
+ async wait_for_text(chat_id, options) {
165
+ const { wait_id, promise } = this.input_manager.create_wait(chat_id, ["text"], options);
166
+ this.dialog_manager.set_waiting(chat_id, wait_id, ["text"]);
167
+ return promise;
168
+ }
169
+ /**
170
+ * Wait for user file upload
171
+ */
172
+ async wait_for_file(chat_id, options) {
173
+ const input_types = options?.input_types ?? ["document"];
174
+ const { wait_id, promise } = this.input_manager.create_wait(chat_id, input_types, options);
175
+ this.dialog_manager.set_waiting(chat_id, wait_id, input_types);
176
+ return promise;
177
+ }
178
+ /**
179
+ * Graceful shutdown
180
+ */
181
+ async stop() {
182
+ this.logger.info("Stopping bot...");
183
+ this.input_manager.clear_all();
184
+ await this.telegram.stopPolling();
185
+ this.logger.info("Bot stopped");
186
+ }
187
+ // ==================== PRIVATE METHODS ====================
188
+ /**
189
+ * Set up all message handlers
190
+ */
191
+ setup_handlers() {
192
+ // IMPORTANT: Register callback_query handler FIRST
193
+ this.telegram.on("callback_query", async (query) => {
194
+ this.logger.debug(`Callback query received: ${query.data}`);
195
+ await this.handle_callback(query);
196
+ });
197
+ // Handle documents
198
+ this.telegram.on("document", async (msg) => {
199
+ this.logger.debug(`Document received from ${msg.chat.id}`);
200
+ await this.handle_document(msg);
201
+ });
202
+ // Handle photos
203
+ this.telegram.on("photo", async (msg) => {
204
+ this.logger.debug(`Photo received from ${msg.chat.id}`);
205
+ await this.handle_photo(msg);
206
+ });
207
+ // Handle contact
208
+ this.telegram.on("contact", async (msg) => {
209
+ this.logger.debug(`Contact received from ${msg.chat.id}`);
210
+ await this.handle_contact(msg);
211
+ });
212
+ // Handle location
213
+ this.telegram.on("location", async (msg) => {
214
+ this.logger.debug(`Location received from ${msg.chat.id}`);
215
+ await this.handle_location(msg);
216
+ });
217
+ // Handle /start command
218
+ if (this.config.enable_start_command) {
219
+ this.telegram.onText(/^\/start(?:\s+(.*))?$/, async (msg, match) => {
220
+ this.logger.debug(`/start command from ${msg.chat.id}`);
221
+ await this.handle_start(msg, match?.[1]);
222
+ });
223
+ }
224
+ // Handle all other commands
225
+ this.telegram.onText(/^\/([a-zA-Z0-9_]+)(?:\s+(.*))?$/, async (msg, match) => {
226
+ const cmd = match?.[1]?.toLowerCase();
227
+ if (cmd === "start" && this.config.enable_start_command) {
228
+ return; // Already handled above
229
+ }
230
+ this.logger.debug(`Command /${cmd} from ${msg.chat.id}`);
231
+ await this.handle_command(msg, cmd ?? "", match?.[2]);
232
+ });
233
+ // Handle regular text messages (NOT commands)
234
+ this.telegram.on("text", async (msg) => {
235
+ // Skip if it's a command
236
+ if (msg.text?.startsWith("/")) {
237
+ return;
238
+ }
239
+ this.logger.debug(`Text message from ${msg.chat.id}: ${msg.text?.substring(0, 50)}`);
240
+ await this.handle_message(msg);
241
+ });
242
+ this.logger.info("All handlers registered");
243
+ }
244
+ /**
245
+ * Handle /start command
246
+ */
247
+ async handle_start(msg, _args) {
248
+ const chat_id = msg.chat.id;
249
+ const should_continue = await this.run_middleware(chat_id, msg, "command");
250
+ if (!should_continue)
251
+ return;
252
+ // Delete the /start message
253
+ if (this.config.auto_delete_user_messages) {
254
+ await this.delete_message(chat_id, msg.message_id);
255
+ }
256
+ // Delete previous bot message if exists
257
+ const state = this.dialog_manager.get_state(chat_id);
258
+ if (state.last_bot_message_id !== -1) {
259
+ await this.delete_message(chat_id, state.last_bot_message_id);
260
+ }
261
+ // Reset and go to start dialog
262
+ this.dialog_manager.reset(chat_id);
263
+ await this.change_dialog(chat_id, this.schema.start_dialog);
264
+ }
265
+ /**
266
+ * Handle other commands
267
+ */
268
+ async handle_command(msg, name, args) {
269
+ const chat_id = msg.chat.id;
270
+ const should_continue = await this.run_middleware(chat_id, msg, "command");
271
+ if (!should_continue)
272
+ return;
273
+ const command = this.schema.commands.get(name.toLowerCase());
274
+ if (!command?.action) {
275
+ this.logger.debug(`Unknown command: /${name}`);
276
+ return;
277
+ }
278
+ const context = {
279
+ chat_id,
280
+ bot: this,
281
+ message: msg,
282
+ command_args: args,
283
+ };
284
+ await this.execute_action(command.action, context);
285
+ }
286
+ /**
287
+ * Handle callback queries (inline button clicks)
288
+ */
289
+ async handle_callback(query) {
290
+ const chat_id = query.message?.chat.id;
291
+ if (!chat_id)
292
+ return;
293
+ // Answer callback to remove loading state
294
+ await this.telegram.answerCallbackQuery(query.id);
295
+ const should_continue = await this.run_middleware(chat_id, query, "callback_query");
296
+ if (!should_continue)
297
+ return;
298
+ // Update last message ID
299
+ if (query.message?.message_id) {
300
+ const state = this.dialog_manager.get_state(chat_id);
301
+ this.dialog_manager.set_last_message(chat_id, query.message.message_id, state.last_message_type);
302
+ }
303
+ const callback_data = query.data;
304
+ if (!callback_data)
305
+ return;
306
+ // Find button
307
+ const registered = this.button_registry.get_inline(callback_data);
308
+ if (!registered) {
309
+ this.logger.warn(`Unknown button callback: ${callback_data}`);
310
+ // Navigate to start dialog as fallback
311
+ await this.change_dialog(chat_id, this.schema.start_dialog);
312
+ return;
313
+ }
314
+ if (registered.button.action) {
315
+ const context = {
316
+ chat_id,
317
+ bot: this,
318
+ callback_query: query,
319
+ };
320
+ await this.execute_action(registered.button.action, context);
321
+ }
322
+ }
323
+ /**
324
+ * Handle regular text messages
325
+ */
326
+ async handle_message(msg) {
327
+ const chat_id = msg.chat.id;
328
+ const should_continue = await this.run_middleware(chat_id, msg, "message");
329
+ if (!should_continue)
330
+ return;
331
+ const state = this.dialog_manager.get_state(chat_id);
332
+ // Check if waiting for input
333
+ if (state.waiting_for_input && has_text(msg)) {
334
+ await this.handle_text_input(chat_id, msg);
335
+ return;
336
+ }
337
+ // Check if it's a reply keyboard button
338
+ if (has_text(msg)) {
339
+ const handled = await this.handle_reply_button(chat_id, msg.text, msg);
340
+ if (handled)
341
+ return;
342
+ }
343
+ // Fallback action
344
+ if (this.schema.fallback_action) {
345
+ const context = {
346
+ chat_id,
347
+ bot: this,
348
+ message: msg,
349
+ };
350
+ await this.execute_action(this.schema.fallback_action, context);
351
+ }
352
+ }
353
+ /**
354
+ * Handle text input when waiting
355
+ */
356
+ async handle_text_input(chat_id, msg) {
357
+ const state = this.dialog_manager.get_state(chat_id);
358
+ const wait_id = state.input_wait_id;
359
+ // Delete user message
360
+ if (this.config.auto_delete_user_messages) {
361
+ await this.delete_message(chat_id, msg.message_id);
362
+ }
363
+ // Check for cancel
364
+ if (this.input_manager.is_cancel_input(msg.text, wait_id)) {
365
+ this.input_manager.cancel_wait(wait_id);
366
+ this.dialog_manager.clear_waiting(chat_id);
367
+ return;
368
+ }
369
+ // Validate
370
+ const validation = await this.input_manager.validate_input(wait_id, msg.text);
371
+ if (validation !== true) {
372
+ const error_msg = typeof validation === "string" ? validation : "Invalid input";
373
+ await this.send_message(chat_id, error_msg);
374
+ return; // Keep waiting
375
+ }
376
+ // Resolve
377
+ this.input_manager.resolve_wait(wait_id, {
378
+ success: true,
379
+ value: msg.text,
380
+ });
381
+ this.dialog_manager.clear_waiting(chat_id);
382
+ }
383
+ /**
384
+ * Handle reply keyboard button press
385
+ */
386
+ async handle_reply_button(chat_id, text, msg) {
387
+ const state = this.dialog_manager.get_state(chat_id);
388
+ const buttons = this.button_registry.find_reply(text, state.current_dialog_id);
389
+ if (buttons.length === 0) {
390
+ // Try without dialog filter (global buttons)
391
+ const global_buttons = this.button_registry.find_reply(text);
392
+ if (global_buttons.length === 0) {
393
+ return false;
394
+ }
395
+ // Use first matching global button
396
+ const button = global_buttons[0];
397
+ if (button.button.action) {
398
+ if (this.config.auto_delete_user_messages) {
399
+ await this.delete_message(chat_id, msg.message_id);
400
+ }
401
+ const context = {
402
+ chat_id,
403
+ bot: this,
404
+ message: msg,
405
+ };
406
+ await this.execute_action(button.button.action, context);
407
+ return true;
408
+ }
409
+ return false;
410
+ }
411
+ // Use first matching button for current dialog
412
+ const button = buttons[0];
413
+ if (button.button.action) {
414
+ if (this.config.auto_delete_user_messages) {
415
+ await this.delete_message(chat_id, msg.message_id);
416
+ }
417
+ const context = {
418
+ chat_id,
419
+ bot: this,
420
+ message: msg,
421
+ };
422
+ await this.execute_action(button.button.action, context);
423
+ return true;
424
+ }
425
+ return false;
426
+ }
427
+ /**
428
+ * Handle document upload
429
+ */
430
+ async handle_document(msg) {
431
+ const chat_id = msg.chat.id;
432
+ const should_continue = await this.run_middleware(chat_id, msg, "document");
433
+ if (!should_continue)
434
+ return;
435
+ const state = this.dialog_manager.get_state(chat_id);
436
+ if (!state.waiting_for_input || !state.input_wait_types.includes("document")) {
437
+ return;
438
+ }
439
+ if (!has_document(msg))
440
+ return;
441
+ // Delete user message
442
+ if (this.config.auto_delete_user_messages) {
443
+ await this.delete_message(chat_id, msg.message_id);
444
+ }
445
+ try {
446
+ const file_path = await this.telegram.downloadFile(msg.document.file_id, "./");
447
+ const content = await readFile(file_path, "utf8");
448
+ await unlink(file_path);
449
+ this.input_manager.resolve_wait(state.input_wait_id, {
450
+ success: true,
451
+ file_id: msg.document.file_id,
452
+ file_content: content,
453
+ file_name: msg.document.file_name,
454
+ mime_type: msg.document.mime_type,
455
+ });
456
+ }
457
+ catch (error) {
458
+ this.input_manager.resolve_wait(state.input_wait_id, {
459
+ success: false,
460
+ error: error.message,
461
+ });
462
+ }
463
+ this.dialog_manager.clear_waiting(chat_id);
464
+ }
465
+ /**
466
+ * Handle photo upload
467
+ */
468
+ async handle_photo(msg) {
469
+ const chat_id = msg.chat.id;
470
+ const should_continue = await this.run_middleware(chat_id, msg, "photo");
471
+ if (!should_continue)
472
+ return;
473
+ const state = this.dialog_manager.get_state(chat_id);
474
+ if (!state.waiting_for_input || !state.input_wait_types.includes("photo")) {
475
+ return;
476
+ }
477
+ if (!has_photo(msg))
478
+ return;
479
+ // Delete user message
480
+ if (this.config.auto_delete_user_messages) {
481
+ await this.delete_message(chat_id, msg.message_id);
482
+ }
483
+ // Get largest photo
484
+ const photo = msg.photo[msg.photo.length - 1];
485
+ this.input_manager.resolve_wait(state.input_wait_id, {
486
+ success: true,
487
+ file_id: photo.file_id,
488
+ });
489
+ this.dialog_manager.clear_waiting(chat_id);
490
+ }
491
+ /**
492
+ * Handle contact share
493
+ */
494
+ async handle_contact(msg) {
495
+ const chat_id = msg.chat.id;
496
+ const should_continue = await this.run_middleware(chat_id, msg, "contact");
497
+ if (!should_continue)
498
+ return;
499
+ if (!has_contact(msg))
500
+ return;
501
+ // Find reply button requesting contact
502
+ // const state = this.dialog_manager.get_state(chat_id);
503
+ // const dialog = this.schema.dialogs.get(state.current_dialog_id);
504
+ // Emit event for handlers
505
+ this.events.emit("contact", chat_id, msg.contact);
506
+ }
507
+ /**
508
+ * Handle location share
509
+ */
510
+ async handle_location(msg) {
511
+ const chat_id = msg.chat.id;
512
+ const should_continue = await this.run_middleware(chat_id, msg, "location");
513
+ if (!should_continue)
514
+ return;
515
+ if (!has_location(msg))
516
+ return;
517
+ // Emit event for handlers
518
+ this.events.emit("location", chat_id, msg.location);
519
+ }
520
+ /**
521
+ * Run middleware chain
522
+ */
523
+ async run_middleware(chat_id, update, update_type) {
524
+ const context = {
525
+ chat_id,
526
+ bot: this,
527
+ update_type,
528
+ user_id: "from" in update ? update.from?.id ?? chat_id : chat_id,
529
+ username: "from" in update ? update.from?.username : undefined,
530
+ timestamp: Date.now(),
531
+ };
532
+ if ("message_id" in update) {
533
+ context.message = update;
534
+ }
535
+ else {
536
+ context.callback_query = update;
537
+ }
538
+ return this.middleware_chain.execute(context, async () => { });
539
+ }
540
+ /**
541
+ * Execute an action or array of actions
542
+ */
543
+ async execute_action(action, context) {
544
+ try {
545
+ if (Array.isArray(action)) {
546
+ for (const act of action) {
547
+ await act(context);
548
+ }
549
+ }
550
+ else {
551
+ await action(context);
552
+ }
553
+ }
554
+ catch (error) {
555
+ this.logger.error(`Action error for chat ${context.chat_id}:`, error);
556
+ // Navigate to error dialog if configured
557
+ if (this.schema.error_dialog) {
558
+ await this.change_dialog(context.chat_id, this.schema.error_dialog);
559
+ }
560
+ }
561
+ }
562
+ /**
563
+ * Render a dialog to the user
564
+ */
565
+ async render_dialog(chat_id, dialog) {
566
+ const state = this.dialog_manager.get_state(chat_id);
567
+ // Resolve text
568
+ const text = await resolve_text(dialog.text, chat_id);
569
+ // Resolve images
570
+ const images = await resolve_images(dialog.images, chat_id);
571
+ // Resolve and build inline keyboard
572
+ let inline_markup;
573
+ const inline_buttons = await resolve_buttons(dialog.inline_buttons, chat_id);
574
+ if (inline_buttons && inline_buttons.length > 0) {
575
+ const built = await this.keyboard_builder.build_inline(dialog.id, inline_buttons, chat_id);
576
+ inline_markup = built;
577
+ }
578
+ // Resolve and build reply keyboard
579
+ let reply_markup;
580
+ if (dialog.remove_reply_keyboard) {
581
+ reply_markup = this.keyboard_builder.build_remove();
582
+ this.dialog_manager.set_reply_keyboard_active(chat_id, false);
583
+ }
584
+ else {
585
+ const reply_buttons = await resolve_buttons(dialog.reply_buttons, chat_id);
586
+ if (reply_buttons && reply_buttons.length > 0) {
587
+ reply_markup = await this.keyboard_builder.build_reply(dialog.id, reply_buttons, chat_id, dialog.reply_keyboard_options);
588
+ this.dialog_manager.set_reply_keyboard_active(chat_id, true);
589
+ }
590
+ }
591
+ // Determine how to send/update message
592
+ try {
593
+ if (images) {
594
+ await this.render_with_images(chat_id, state, text, images, inline_markup, reply_markup, dialog);
595
+ }
596
+ else {
597
+ await this.render_text_only(chat_id, state, text, inline_markup, reply_markup, dialog);
598
+ }
599
+ }
600
+ catch (error) {
601
+ const tg_error = TelegramError.from_error(error, chat_id);
602
+ // Ignore "message not modified" errors
603
+ if (tg_error.is_message_not_modified()) {
604
+ return;
605
+ }
606
+ // Try to recover by sending new message
607
+ this.logger.warn(`Failed to update message, sending new: ${tg_error.message}`);
608
+ await this.send_new_message(chat_id, state, text, inline_markup, reply_markup, dialog);
609
+ }
610
+ }
611
+ /**
612
+ * Render dialog with images
613
+ */
614
+ async render_with_images(chat_id, state, text, images, inline_markup, reply_markup, dialog) {
615
+ // Delete previous message if it was text-only
616
+ if (state.last_message_type === "text" && state.last_bot_message_id !== -1) {
617
+ await this.delete_message(chat_id, state.last_bot_message_id);
618
+ }
619
+ const image_array = Array.isArray(images) ? images : [images];
620
+ if (image_array.length > 1) {
621
+ // Send media group (no inline buttons support)
622
+ const media = image_array.map((img, idx) => ({
623
+ type: "photo",
624
+ media: img,
625
+ caption: idx === 0 ? text : undefined,
626
+ parse_mode: this.config.default_parse_mode,
627
+ }));
628
+ const result = await this.telegram.sendMediaGroup(chat_id, media);
629
+ const last_msg = result[result.length - 1];
630
+ this.dialog_manager.set_last_message(chat_id, last_msg.message_id, "media_group");
631
+ // Send reply keyboard separately if needed
632
+ if (reply_markup && "keyboard" in reply_markup) {
633
+ await this.telegram.sendMessage(chat_id, "⌨️", { reply_markup });
634
+ }
635
+ }
636
+ else {
637
+ // Send single photo
638
+ const result = await this.telegram.sendPhoto(chat_id, image_array[0], {
639
+ caption: text,
640
+ parse_mode: this.config.default_parse_mode,
641
+ reply_markup: inline_markup ?? reply_markup,
642
+ protect_content: dialog.protect_content,
643
+ });
644
+ this.dialog_manager.set_last_message(chat_id, result.message_id, "photo");
645
+ }
646
+ }
647
+ /**
648
+ * Render text-only dialog
649
+ */
650
+ async render_text_only(chat_id, state, text, inline_markup, reply_markup, dialog) {
651
+ const display_text = text ?? "📄";
652
+ // Try to edit existing message if it was text
653
+ if (state.last_message_type === "text" && state.last_bot_message_id !== -1) {
654
+ await this.telegram.editMessageText(display_text, {
655
+ chat_id,
656
+ message_id: state.last_bot_message_id,
657
+ parse_mode: this.config.default_parse_mode,
658
+ reply_markup: inline_markup,
659
+ disable_web_page_preview: dialog.disable_web_page_preview,
660
+ });
661
+ // Update reply keyboard separately if needed
662
+ if (reply_markup) {
663
+ // Can't edit reply keyboard, need to send new message or use sendChatAction
664
+ // For now, we skip reply keyboard update on edit
665
+ }
666
+ }
667
+ else {
668
+ // Delete old message if different type
669
+ if (state.last_bot_message_id !== -1) {
670
+ await this.delete_message(chat_id, state.last_bot_message_id);
671
+ }
672
+ await this.send_new_message(chat_id, state, text, inline_markup, reply_markup, dialog);
673
+ }
674
+ }
675
+ /**
676
+ * Send a new message (used as fallback)
677
+ */
678
+ async send_new_message(chat_id, _state, text, inline_markup, reply_markup, dialog) {
679
+ const display_text = text ?? "📄";
680
+ const result = await this.telegram.sendMessage(chat_id, display_text, {
681
+ parse_mode: this.config.default_parse_mode,
682
+ reply_markup: inline_markup ?? reply_markup,
683
+ disable_web_page_preview: dialog.disable_web_page_preview,
684
+ protect_content: dialog.protect_content,
685
+ });
686
+ this.dialog_manager.set_last_message(chat_id, result.message_id, "text");
687
+ }
688
+ }
689
+ //# sourceMappingURL=bot_builder.js.map