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