telegram-botbuilder 1.6.5 → 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.
- package/Changelog.md +81 -0
- package/MIGRATION.md +453 -0
- package/README.md +403 -0
- package/lib/actions/callback_action.d.ts +14 -0
- package/lib/actions/callback_action.d.ts.map +1 -0
- package/lib/actions/callback_action.js +25 -0
- package/lib/actions/callback_action.js.map +1 -0
- package/lib/actions/change_dialog.d.ts +10 -0
- package/lib/actions/change_dialog.d.ts.map +1 -0
- package/lib/actions/change_dialog.js +17 -0
- package/lib/actions/change_dialog.js.map +1 -0
- package/lib/actions/control_flow.d.ts +26 -0
- package/lib/actions/control_flow.d.ts.map +1 -0
- package/lib/actions/control_flow.js +61 -0
- package/lib/actions/control_flow.js.map +1 -0
- package/lib/actions/index.d.ts +6 -0
- package/lib/actions/index.d.ts.map +1 -0
- package/lib/actions/index.js +11 -0
- package/lib/actions/index.js.map +1 -0
- package/lib/actions/send_message.d.ts +16 -0
- package/lib/actions/send_message.d.ts.map +1 -0
- package/lib/actions/send_message.js +32 -0
- package/lib/actions/send_message.js.map +1 -0
- package/lib/actions/wait_for_input.d.ts +14 -0
- package/lib/actions/wait_for_input.d.ts.map +1 -0
- package/lib/actions/wait_for_input.js +54 -0
- package/lib/actions/wait_for_input.js.map +1 -0
- package/lib/core/bot_builder.d.ts +145 -0
- package/lib/core/bot_builder.d.ts.map +1 -0
- package/lib/core/bot_builder.js +678 -0
- package/lib/core/bot_builder.js.map +1 -0
- package/lib/core/button_registry.d.ts +39 -0
- package/lib/core/button_registry.d.ts.map +1 -0
- package/lib/core/button_registry.js +84 -0
- package/lib/core/button_registry.js.map +1 -0
- package/lib/core/dialog_manager.d.ts +71 -0
- package/lib/core/dialog_manager.d.ts.map +1 -0
- package/lib/core/dialog_manager.js +146 -0
- package/lib/core/dialog_manager.js.map +1 -0
- package/lib/core/index.d.ts +8 -0
- package/lib/core/index.d.ts.map +1 -0
- package/lib/core/index.js +8 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/input_manager.d.ts +49 -0
- package/lib/core/input_manager.d.ts.map +1 -0
- package/lib/core/input_manager.js +129 -0
- package/lib/core/input_manager.js.map +1 -0
- package/lib/core/keyboard_builder.d.ts +35 -0
- package/lib/core/keyboard_builder.d.ts.map +1 -0
- package/lib/core/keyboard_builder.js +87 -0
- package/lib/core/keyboard_builder.js.map +1 -0
- package/lib/core/middleware_chain.d.ts +25 -0
- package/lib/core/middleware_chain.d.ts.map +1 -0
- package/lib/core/middleware_chain.js +54 -0
- package/lib/core/middleware_chain.js.map +1 -0
- package/lib/core/schema_compiler.d.ts +25 -0
- package/lib/core/schema_compiler.d.ts.map +1 -0
- package/lib/core/schema_compiler.js +65 -0
- package/lib/core/schema_compiler.js.map +1 -0
- package/lib/errors/bot_error.d.ts +7 -0
- package/lib/errors/bot_error.d.ts.map +1 -0
- package/lib/errors/bot_error.js +17 -0
- package/lib/errors/bot_error.js.map +1 -0
- package/lib/errors/dialog_error.d.ts +12 -0
- package/lib/errors/dialog_error.d.ts.map +1 -0
- package/lib/errors/dialog_error.js +22 -0
- package/lib/errors/dialog_error.js.map +1 -0
- package/lib/errors/index.d.ts +5 -0
- package/lib/errors/index.d.ts.map +1 -0
- package/lib/errors/index.js +5 -0
- package/lib/errors/index.js.map +1 -0
- package/lib/errors/telegram_error.d.ts +12 -0
- package/lib/errors/telegram_error.d.ts.map +1 -0
- package/lib/errors/telegram_error.js +39 -0
- package/lib/errors/telegram_error.js.map +1 -0
- package/lib/errors/validation_error.d.ts +19 -0
- package/lib/errors/validation_error.d.ts.map +1 -0
- package/lib/errors/validation_error.js +35 -0
- package/lib/errors/validation_error.js.map +1 -0
- package/lib/index.d.ts +6 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +9 -18
- package/lib/index.js.map +1 -1
- package/lib/types/action.d.ts +58 -0
- package/lib/types/action.d.ts.map +1 -0
- package/lib/types/action.js +2 -0
- package/lib/types/action.js.map +1 -0
- package/lib/types/config.d.ts +36 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +12 -0
- package/lib/types/config.js.map +1 -0
- package/lib/types/dialog.d.ts +39 -0
- package/lib/types/dialog.d.ts.map +1 -0
- package/lib/types/dialog.js +2 -0
- package/lib/types/dialog.js.map +1 -0
- package/lib/types/index.d.ts +10 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +3 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/internal.d.ts +58 -0
- package/lib/types/internal.d.ts.map +1 -0
- package/lib/types/internal.js +11 -0
- package/lib/types/internal.js.map +1 -0
- package/lib/types/keyboard.d.ts +41 -0
- package/lib/types/keyboard.d.ts.map +1 -0
- package/lib/types/keyboard.js +2 -0
- package/lib/types/keyboard.js.map +1 -0
- package/lib/types/middleware.d.ts +24 -0
- package/lib/types/middleware.d.ts.map +1 -0
- package/lib/types/middleware.js +2 -0
- package/lib/types/middleware.js.map +1 -0
- package/lib/types/schema.d.ts +17 -0
- package/lib/types/schema.d.ts.map +1 -0
- package/lib/types/schema.js +2 -0
- package/lib/types/schema.js.map +1 -0
- package/lib/utils/constants.d.ts +33 -0
- package/lib/utils/constants.d.ts.map +1 -0
- package/lib/utils/constants.js +33 -0
- package/lib/utils/constants.js.map +1 -0
- package/lib/utils/deep_copy.d.ts +6 -0
- package/lib/utils/deep_copy.d.ts.map +1 -0
- package/lib/utils/deep_copy.js +45 -0
- package/lib/utils/deep_copy.js.map +1 -0
- package/lib/utils/hash.d.ts +17 -0
- package/lib/utils/hash.d.ts.map +1 -0
- package/lib/utils/hash.js +50 -0
- package/lib/utils/hash.js.map +1 -0
- package/lib/utils/index.d.ts +7 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +7 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/logger.d.ts +17 -0
- package/lib/utils/logger.d.ts.map +1 -0
- package/lib/utils/logger.js +91 -0
- package/lib/utils/logger.js.map +1 -0
- package/lib/utils/resolvers.d.ts +29 -0
- package/lib/utils/resolvers.d.ts.map +1 -0
- package/lib/utils/resolvers.js +60 -0
- package/lib/utils/resolvers.js.map +1 -0
- package/lib/utils/type_guards.d.ts +22 -0
- package/lib/utils/type_guards.d.ts.map +1 -0
- package/lib/utils/type_guards.js +38 -0
- package/lib/utils/type_guards.js.map +1 -0
- package/lib/validation/index.d.ts +2 -0
- package/lib/validation/index.d.ts.map +1 -0
- package/lib/validation/index.js +2 -0
- package/lib/validation/index.js.map +1 -0
- package/lib/validation/schema_validator.d.ts +11 -0
- package/lib/validation/schema_validator.d.ts.map +1 -0
- package/lib/validation/schema_validator.js +156 -0
- package/lib/validation/schema_validator.js.map +1 -0
- package/package.json +59 -15
- package/lib/bot-service.d.ts +0 -27
- package/lib/bot-service.d.ts.map +0 -1
- package/lib/bot-service.js +0 -326
- package/lib/bot-service.js.map +0 -1
- package/lib/bot-struct.d.ts +0 -58
- package/lib/bot-struct.d.ts.map +0 -1
- package/lib/bot-struct.js +0 -3
- package/lib/bot-struct.js.map +0 -1
- package/readme.md +0 -54
- package/src/bot-service.ts +0 -289
- package/src/bot-struct.ts +0 -59
- package/src/index.ts +0 -2
- 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
|