@grinev/opencode-telegram-bot 0.4.0 → 0.5.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/README.md +9 -1
- package/dist/bot/commands/definitions.js +7 -4
- package/dist/bot/commands/help.js +7 -1
- package/dist/bot/commands/new.js +2 -0
- package/dist/bot/commands/projects.js +18 -9
- package/dist/bot/commands/rename.js +49 -1
- package/dist/bot/commands/sessions.js +13 -2
- package/dist/bot/commands/stop.js +3 -5
- package/dist/bot/handlers/agent.js +12 -1
- package/dist/bot/handlers/context.js +15 -25
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +15 -2
- package/dist/bot/handlers/permission.js +50 -5
- package/dist/bot/handlers/question.js +97 -9
- package/dist/bot/handlers/variant.js +12 -1
- package/dist/bot/index.js +49 -9
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/i18n/en.js +23 -4
- package/dist/i18n/ru.js +23 -4
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/guard.js +87 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/permission/manager.js +6 -0
- package/dist/question/manager.js +33 -0
- package/dist/rename/manager.js +3 -0
- package/package.json +1 -1
package/dist/i18n/en.js
CHANGED
|
@@ -16,6 +16,19 @@ export const en = {
|
|
|
16
16
|
"error.load_variants": "❌ Failed to load variants list",
|
|
17
17
|
"error.context_button": "❌ Failed to process context button",
|
|
18
18
|
"error.generic": "🔴 Something went wrong.",
|
|
19
|
+
"interaction.blocked.expired": "⚠️ This interaction has expired. Please start it again.",
|
|
20
|
+
"interaction.blocked.expected_callback": "⚠️ Please use the inline buttons for this step or tap Cancel.",
|
|
21
|
+
"interaction.blocked.expected_text": "⚠️ Please send a text message for this step.",
|
|
22
|
+
"interaction.blocked.expected_command": "⚠️ Please send a command for this step.",
|
|
23
|
+
"interaction.blocked.command_not_allowed": "⚠️ This command is not available in the current step.",
|
|
24
|
+
"interaction.blocked.finish_current": "⚠️ Finish the current interaction first (answer or cancel), then open another menu.",
|
|
25
|
+
"inline.blocked.expected_choice": "⚠️ Choose an option using the inline buttons or tap Cancel.",
|
|
26
|
+
"inline.blocked.command_not_allowed": "⚠️ This command is not available while inline menu is active.",
|
|
27
|
+
"question.blocked.expected_answer": "⚠️ Answer the current question using buttons, Custom answer, or Cancel.",
|
|
28
|
+
"question.blocked.command_not_allowed": "⚠️ This command is not available until current question flow is completed.",
|
|
29
|
+
"inline.button.cancel": "❌ Cancel",
|
|
30
|
+
"inline.inactive_callback": "This menu is inactive",
|
|
31
|
+
"inline.cancelled_callback": "Cancelled",
|
|
19
32
|
"common.unknown": "unknown",
|
|
20
33
|
"common.unknown_error": "unknown error",
|
|
21
34
|
"start.welcome": "👋 Welcome to OpenCode Telegram Bot!\n\nUse commands:\n/projects — select project\n/sessions — session list\n/new — new session\n/agent — switch mode\n/model — select model\n/status — status\n/help — help",
|
|
@@ -29,6 +42,7 @@ export const en = {
|
|
|
29
42
|
"bot.session_reset_project_mismatch": "⚠️ Active session does not match the selected project, so it was reset. Use /sessions to pick one or /new to create a new session.",
|
|
30
43
|
"bot.prompt_send_error_detailed": "🔴 Failed to send request.\n\nDetails: {details}",
|
|
31
44
|
"bot.prompt_send_error": "🔴 An error occurred while sending request to OpenCode.",
|
|
45
|
+
"bot.unknown_command": "⚠️ Unknown command: {command}. Use /help to see available commands.",
|
|
32
46
|
"status.header_running": "🟢 **OpenCode Server is running**",
|
|
33
47
|
"status.health.healthy": "Healthy",
|
|
34
48
|
"status.health.unhealthy": "Unhealthy",
|
|
@@ -111,7 +125,6 @@ export const en = {
|
|
|
111
125
|
"variant.menu.current": "Current variant: {name}\n\nSelect variant:",
|
|
112
126
|
"variant.menu.error": "🔴 Failed to get variants list",
|
|
113
127
|
"context.button.confirm": "✅ Yes, compact context",
|
|
114
|
-
"context.button.cancel": "❌ Cancel",
|
|
115
128
|
"context.no_active_session": "⚠️ No active session. Create a session with /new",
|
|
116
129
|
"context.confirm_text": '📊 Context compaction for session "{title}"\n\nThis will reduce context usage by removing old messages from history. Current task will not be interrupted.\n\nContinue?',
|
|
117
130
|
"context.callback_session_not_found": "Session not found",
|
|
@@ -119,7 +132,6 @@ export const en = {
|
|
|
119
132
|
"context.progress": "⏳ Compacting context...",
|
|
120
133
|
"context.error": "❌ Context compaction failed",
|
|
121
134
|
"context.success": "✅ Context compacted successfully",
|
|
122
|
-
"context.callback_cancelled": "Cancelled",
|
|
123
135
|
"permission.inactive_callback": "Permission request is inactive",
|
|
124
136
|
"permission.processing_error_callback": "Processing error",
|
|
125
137
|
"permission.no_active_request_callback": "Error: no active request",
|
|
@@ -127,9 +139,11 @@ export const en = {
|
|
|
127
139
|
"permission.reply.always": "Always allowed",
|
|
128
140
|
"permission.reply.reject": "Rejected",
|
|
129
141
|
"permission.send_reply_error": "❌ Failed to send permission reply",
|
|
142
|
+
"permission.blocked.expected_reply": "⚠️ Please answer the permission request first using the buttons above.",
|
|
143
|
+
"permission.blocked.command_not_allowed": "⚠️ This command is not available until you answer the permission request.",
|
|
130
144
|
"permission.header": "{emoji} **Permission request: {name}**\n\n",
|
|
131
|
-
"permission.button.allow": "✅ Allow",
|
|
132
|
-
"permission.button.always": "🔓
|
|
145
|
+
"permission.button.allow": "✅ Allow once",
|
|
146
|
+
"permission.button.always": "🔓 Allow always",
|
|
133
147
|
"permission.button.reject": "❌ Reject",
|
|
134
148
|
"permission.name.bash": "Bash",
|
|
135
149
|
"permission.name.edit": "Edit",
|
|
@@ -156,6 +170,7 @@ export const en = {
|
|
|
156
170
|
"question.button.submit": "✅ Done",
|
|
157
171
|
"question.button.custom": "🔤 Custom answer",
|
|
158
172
|
"question.button.cancel": "❌ Cancel",
|
|
173
|
+
"question.use_custom_button_first": '⚠️ To send text, tap "Custom answer" for the current question first.',
|
|
159
174
|
"question.summary.title": "✅ Poll completed!\n\n",
|
|
160
175
|
"question.summary.question": "Question {index}:\n{question}\n\n",
|
|
161
176
|
"question.summary.answer": "Answer:\n{answer}\n\n",
|
|
@@ -193,6 +208,10 @@ export const en = {
|
|
|
193
208
|
"rename.success": "✅ Session renamed to: {title}",
|
|
194
209
|
"rename.error": "🔴 Failed to rename session.",
|
|
195
210
|
"rename.cancelled": "❌ Rename cancelled.",
|
|
211
|
+
"rename.inactive_callback": "Rename request is inactive",
|
|
212
|
+
"rename.inactive": "⚠️ Rename request is not active. Run /rename again.",
|
|
213
|
+
"rename.blocked.expected_name": "⚠️ Enter a new session name as text or tap Cancel in rename message.",
|
|
214
|
+
"rename.blocked.command_not_allowed": "⚠️ This command is not available while rename is waiting for a new name.",
|
|
196
215
|
"rename.button.cancel": "❌ Cancel",
|
|
197
216
|
"cmd.description.rename": "Rename current session",
|
|
198
217
|
"cli.usage": "Usage:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nNotes:\n - No command defaults to `start`\n - `--mode` is currently supported for `start` only",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -16,6 +16,19 @@ export const ru = {
|
|
|
16
16
|
"error.load_variants": "❌ Ошибка при загрузке списка вариантов",
|
|
17
17
|
"error.context_button": "❌ Ошибка при обработке кнопки контекста",
|
|
18
18
|
"error.generic": "🔴 Произошла ошибка.",
|
|
19
|
+
"interaction.blocked.expired": "⚠️ Текущая интеракция устарела. Запустите ее снова.",
|
|
20
|
+
"interaction.blocked.expected_callback": "⚠️ Для этого шага используйте inline-кнопки или нажмите Отмена.",
|
|
21
|
+
"interaction.blocked.expected_text": "⚠️ Для этого шага отправьте текстовое сообщение.",
|
|
22
|
+
"interaction.blocked.expected_command": "⚠️ Для этого шага отправьте команду.",
|
|
23
|
+
"interaction.blocked.command_not_allowed": "⚠️ Эта команда недоступна на текущем шаге.",
|
|
24
|
+
"interaction.blocked.finish_current": "⚠️ Сначала завершите текущую интеракцию (ответьте или отмените), затем откройте другое меню.",
|
|
25
|
+
"inline.blocked.expected_choice": "⚠️ Выберите вариант через inline-кнопки или нажмите Отмена.",
|
|
26
|
+
"inline.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока активно inline-меню.",
|
|
27
|
+
"question.blocked.expected_answer": "⚠️ Ответьте на текущий вопрос кнопками, через Свой ответ, или нажмите Отмена.",
|
|
28
|
+
"question.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока не завершен текущий опрос.",
|
|
29
|
+
"inline.button.cancel": "❌ Отмена",
|
|
30
|
+
"inline.inactive_callback": "Это меню уже неактивно",
|
|
31
|
+
"inline.cancelled_callback": "Отменено",
|
|
19
32
|
"common.unknown": "неизвестна",
|
|
20
33
|
"common.unknown_error": "неизвестная ошибка",
|
|
21
34
|
"start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/agent — сменить режим\n/model — выбрать модель\n/status — статус\n/help — справка",
|
|
@@ -29,6 +42,7 @@ export const ru = {
|
|
|
29
42
|
"bot.session_reset_project_mismatch": "⚠️ Активная сессия не соответствует выбранному проекту, поэтому была сброшена. Используйте /sessions для выбора или /new для создания новой сессии.",
|
|
30
43
|
"bot.prompt_send_error_detailed": "🔴 Ошибка при отправке запроса.\n\nДетали: {details}",
|
|
31
44
|
"bot.prompt_send_error": "🔴 Произошла ошибка при отправке запроса в OpenCode.",
|
|
45
|
+
"bot.unknown_command": "⚠️ Неизвестная команда: {command}. Используйте /help для списка команд.",
|
|
32
46
|
"status.header_running": "🟢 **OpenCode Server запущен**",
|
|
33
47
|
"status.health.healthy": "Healthy",
|
|
34
48
|
"status.health.unhealthy": "Unhealthy",
|
|
@@ -111,7 +125,6 @@ export const ru = {
|
|
|
111
125
|
"variant.menu.current": "Текущий variant: {name}\n\nВыберите variant:",
|
|
112
126
|
"variant.menu.error": "🔴 Не удалось получить список вариантов",
|
|
113
127
|
"context.button.confirm": "✅ Да, сжать контекст",
|
|
114
|
-
"context.button.cancel": "❌ Отмена",
|
|
115
128
|
"context.no_active_session": "⚠️ Нет активной сессии. Создайте сессию командой /new",
|
|
116
129
|
"context.confirm_text": '📊 Сжатие контекста для сессии "{title}"\n\nЭто уменьшит использование контекста, удалив старые сообщения из истории. Текущая задача не будет прервана.\n\nПродолжить?',
|
|
117
130
|
"context.callback_session_not_found": "Сессия не найдена",
|
|
@@ -119,7 +132,6 @@ export const ru = {
|
|
|
119
132
|
"context.progress": "⏳ Сжимаю контекст...",
|
|
120
133
|
"context.error": "❌ Ошибка при сжатии контекста",
|
|
121
134
|
"context.success": "✅ Контекст успешно сжат",
|
|
122
|
-
"context.callback_cancelled": "Отменено",
|
|
123
135
|
"permission.inactive_callback": "Запрос разрешения неактивен",
|
|
124
136
|
"permission.processing_error_callback": "Ошибка при обработке",
|
|
125
137
|
"permission.no_active_request_callback": "Ошибка: нет активного запроса",
|
|
@@ -127,9 +139,11 @@ export const ru = {
|
|
|
127
139
|
"permission.reply.always": "Разрешено всегда",
|
|
128
140
|
"permission.reply.reject": "Отклонено",
|
|
129
141
|
"permission.send_reply_error": "❌ Не удалось отправить ответ на запрос разрешения",
|
|
142
|
+
"permission.blocked.expected_reply": "⚠️ Сначала ответьте на запрос разрешения кнопками выше.",
|
|
143
|
+
"permission.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока вы не ответите на запрос разрешения.",
|
|
130
144
|
"permission.header": "{emoji} **Запрос разрешения: {name}**\n\n",
|
|
131
|
-
"permission.button.allow": "✅ Разрешить",
|
|
132
|
-
"permission.button.always": "🔓
|
|
145
|
+
"permission.button.allow": "✅ Разрешить один раз",
|
|
146
|
+
"permission.button.always": "🔓 Разрешить всегда",
|
|
133
147
|
"permission.button.reject": "❌ Отклонить",
|
|
134
148
|
"permission.name.bash": "Bash",
|
|
135
149
|
"permission.name.edit": "Edit",
|
|
@@ -156,6 +170,7 @@ export const ru = {
|
|
|
156
170
|
"question.button.submit": "✅ Готово",
|
|
157
171
|
"question.button.custom": "🔤 Свой ответ",
|
|
158
172
|
"question.button.cancel": "❌ Отмена",
|
|
173
|
+
"question.use_custom_button_first": '⚠️ Чтобы отправить текст, сначала нажмите кнопку "Свой ответ" для текущего вопроса.',
|
|
159
174
|
"question.summary.title": "✅ Опрос завершен!\n\n",
|
|
160
175
|
"question.summary.question": "Вопрос {index}:\n{question}\n\n",
|
|
161
176
|
"question.summary.answer": "Ответ:\n{answer}\n\n",
|
|
@@ -193,6 +208,10 @@ export const ru = {
|
|
|
193
208
|
"rename.success": "✅ Сессия переименована в: {title}",
|
|
194
209
|
"rename.error": "🔴 Не удалось переименовать сессию.",
|
|
195
210
|
"rename.cancelled": "❌ Переименование отменено.",
|
|
211
|
+
"rename.inactive_callback": "Запрос переименования неактивен",
|
|
212
|
+
"rename.inactive": "⚠️ Запрос переименования неактивен. Выполните /rename снова.",
|
|
213
|
+
"rename.blocked.expected_name": "⚠️ Введите новое название текстом или нажмите Отмена в сообщении переименования.",
|
|
214
|
+
"rename.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока ожидается новое название сессии.",
|
|
196
215
|
"rename.button.cancel": "❌ Отмена",
|
|
197
216
|
"cmd.description.rename": "Переименовать текущую сессию",
|
|
198
217
|
"cli.usage": "Использование:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nЗаметки:\n - Без команды по умолчанию используется `start`\n - `--mode` сейчас поддерживается только для `start`",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { permissionManager } from "../permission/manager.js";
|
|
2
|
+
import { questionManager } from "../question/manager.js";
|
|
3
|
+
import { renameManager } from "../rename/manager.js";
|
|
4
|
+
import { interactionManager } from "./manager.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
export function clearAllInteractionState(reason) {
|
|
7
|
+
const questionActive = questionManager.isActive();
|
|
8
|
+
const permissionActive = permissionManager.isActive();
|
|
9
|
+
const renameActive = renameManager.isWaitingForName();
|
|
10
|
+
const interactionSnapshot = interactionManager.getSnapshot();
|
|
11
|
+
questionManager.clear();
|
|
12
|
+
permissionManager.clear();
|
|
13
|
+
renameManager.clear();
|
|
14
|
+
interactionManager.clear(reason);
|
|
15
|
+
const hasAnyActiveState = questionActive || permissionActive || renameActive || interactionSnapshot !== null;
|
|
16
|
+
const message = `[InteractionCleanup] Cleared state: reason=${reason}, ` +
|
|
17
|
+
`questionActive=${questionActive}, permissionActive=${permissionActive}, ` +
|
|
18
|
+
`renameActive=${renameActive}, interactionKind=${interactionSnapshot?.kind || "none"}`;
|
|
19
|
+
if (hasAnyActiveState) {
|
|
20
|
+
logger.info(message);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
logger.debug(message);
|
|
24
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { interactionManager } from "./manager.js";
|
|
2
|
+
function normalizeIncomingCommand(text) {
|
|
3
|
+
const trimmed = text.trim();
|
|
4
|
+
if (!trimmed.startsWith("/")) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const token = trimmed.split(/\s+/)[0];
|
|
8
|
+
const withoutMention = token.split("@")[0].toLowerCase();
|
|
9
|
+
if (withoutMention.length <= 1) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return withoutMention;
|
|
13
|
+
}
|
|
14
|
+
function classifyIncomingInput(ctx) {
|
|
15
|
+
if (ctx.callbackQuery?.data) {
|
|
16
|
+
return { inputType: "callback" };
|
|
17
|
+
}
|
|
18
|
+
const text = ctx.message?.text;
|
|
19
|
+
if (typeof text === "string") {
|
|
20
|
+
const command = normalizeIncomingCommand(text);
|
|
21
|
+
if (command) {
|
|
22
|
+
return { inputType: "command", command };
|
|
23
|
+
}
|
|
24
|
+
return { inputType: "text" };
|
|
25
|
+
}
|
|
26
|
+
return { inputType: "other" };
|
|
27
|
+
}
|
|
28
|
+
function getExpectedInputBlockReason(expectedInput) {
|
|
29
|
+
switch (expectedInput) {
|
|
30
|
+
case "callback":
|
|
31
|
+
return "expected_callback";
|
|
32
|
+
case "command":
|
|
33
|
+
return "expected_command";
|
|
34
|
+
case "text":
|
|
35
|
+
case "mixed":
|
|
36
|
+
return "expected_text";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function createAllowDecision(inputType, state, command) {
|
|
40
|
+
return {
|
|
41
|
+
allow: true,
|
|
42
|
+
inputType,
|
|
43
|
+
state,
|
|
44
|
+
command,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createBlockDecision(inputType, state, reason, command) {
|
|
48
|
+
return {
|
|
49
|
+
allow: false,
|
|
50
|
+
inputType,
|
|
51
|
+
state,
|
|
52
|
+
reason,
|
|
53
|
+
command,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isAllowedRenameCancelCallback(ctx, state) {
|
|
57
|
+
return (state.kind === "rename" &&
|
|
58
|
+
state.expectedInput === "text" &&
|
|
59
|
+
ctx.callbackQuery?.data === "rename:cancel");
|
|
60
|
+
}
|
|
61
|
+
export function resolveInteractionGuardDecision(ctx) {
|
|
62
|
+
const state = interactionManager.getSnapshot();
|
|
63
|
+
const { inputType, command } = classifyIncomingInput(ctx);
|
|
64
|
+
if (!state) {
|
|
65
|
+
return createAllowDecision(inputType, null, command);
|
|
66
|
+
}
|
|
67
|
+
if (interactionManager.isExpired()) {
|
|
68
|
+
interactionManager.clear("expired");
|
|
69
|
+
return createBlockDecision(inputType, state, "expired", command);
|
|
70
|
+
}
|
|
71
|
+
if (inputType === "command") {
|
|
72
|
+
if (command && state.allowedCommands.includes(command)) {
|
|
73
|
+
return createAllowDecision(inputType, state, command);
|
|
74
|
+
}
|
|
75
|
+
return createBlockDecision(inputType, state, "command_not_allowed", command);
|
|
76
|
+
}
|
|
77
|
+
if (state.expectedInput === "mixed") {
|
|
78
|
+
return createAllowDecision(inputType, state, command);
|
|
79
|
+
}
|
|
80
|
+
if (inputType === "callback" && isAllowedRenameCancelCallback(ctx, state)) {
|
|
81
|
+
return createAllowDecision(inputType, state, command);
|
|
82
|
+
}
|
|
83
|
+
if (state.expectedInput === inputType) {
|
|
84
|
+
return createAllowDecision(inputType, state, command);
|
|
85
|
+
}
|
|
86
|
+
return createBlockDecision(inputType, state, getExpectedInputBlockReason(state.expectedInput), command);
|
|
87
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/stop"];
|
|
3
|
+
function normalizeCommand(command) {
|
|
4
|
+
const trimmed = command.trim().toLowerCase();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
9
|
+
const withoutMention = withSlash.split("@")[0];
|
|
10
|
+
if (withoutMention.length <= 1) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return withoutMention;
|
|
14
|
+
}
|
|
15
|
+
function normalizeAllowedCommands(commands) {
|
|
16
|
+
if (commands === undefined) {
|
|
17
|
+
return [...DEFAULT_ALLOWED_INTERACTION_COMMANDS];
|
|
18
|
+
}
|
|
19
|
+
const normalized = new Set();
|
|
20
|
+
for (const command of commands) {
|
|
21
|
+
const value = normalizeCommand(command);
|
|
22
|
+
if (value) {
|
|
23
|
+
normalized.add(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return Array.from(normalized);
|
|
27
|
+
}
|
|
28
|
+
function cloneState(state) {
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
allowedCommands: [...state.allowedCommands],
|
|
32
|
+
metadata: { ...state.metadata },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
class InteractionManager {
|
|
36
|
+
state = null;
|
|
37
|
+
start(options) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
let expiresAt = null;
|
|
40
|
+
if (this.state) {
|
|
41
|
+
this.clear("state_replaced");
|
|
42
|
+
}
|
|
43
|
+
if (typeof options.expiresInMs === "number") {
|
|
44
|
+
expiresAt = now + options.expiresInMs;
|
|
45
|
+
}
|
|
46
|
+
const nextState = {
|
|
47
|
+
kind: options.kind,
|
|
48
|
+
expectedInput: options.expectedInput,
|
|
49
|
+
allowedCommands: normalizeAllowedCommands(options.allowedCommands),
|
|
50
|
+
metadata: options.metadata ? { ...options.metadata } : {},
|
|
51
|
+
createdAt: now,
|
|
52
|
+
expiresAt,
|
|
53
|
+
};
|
|
54
|
+
this.state = nextState;
|
|
55
|
+
logger.info(`[InteractionManager] Started interaction: kind=${nextState.kind}, expectedInput=${nextState.expectedInput}, allowedCommands=${nextState.allowedCommands.join(",") || "none"}`);
|
|
56
|
+
return cloneState(nextState);
|
|
57
|
+
}
|
|
58
|
+
get() {
|
|
59
|
+
if (!this.state) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return cloneState(this.state);
|
|
63
|
+
}
|
|
64
|
+
getSnapshot() {
|
|
65
|
+
return this.get();
|
|
66
|
+
}
|
|
67
|
+
isActive() {
|
|
68
|
+
return this.state !== null;
|
|
69
|
+
}
|
|
70
|
+
isExpired(referenceTimeMs = Date.now()) {
|
|
71
|
+
if (!this.state || this.state.expiresAt === null) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return referenceTimeMs >= this.state.expiresAt;
|
|
75
|
+
}
|
|
76
|
+
transition(options) {
|
|
77
|
+
if (!this.state) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
this.state = {
|
|
82
|
+
...this.state,
|
|
83
|
+
kind: options.kind ?? this.state.kind,
|
|
84
|
+
expectedInput: options.expectedInput ?? this.state.expectedInput,
|
|
85
|
+
allowedCommands: options.allowedCommands !== undefined
|
|
86
|
+
? normalizeAllowedCommands(options.allowedCommands)
|
|
87
|
+
: [...this.state.allowedCommands],
|
|
88
|
+
metadata: options.metadata ? { ...options.metadata } : { ...this.state.metadata },
|
|
89
|
+
expiresAt: options.expiresInMs === undefined
|
|
90
|
+
? this.state.expiresAt
|
|
91
|
+
: options.expiresInMs === null
|
|
92
|
+
? null
|
|
93
|
+
: now + options.expiresInMs,
|
|
94
|
+
};
|
|
95
|
+
logger.debug(`[InteractionManager] Transitioned interaction: kind=${this.state.kind}, expectedInput=${this.state.expectedInput}, allowedCommands=${this.state.allowedCommands.join(",") || "none"}`);
|
|
96
|
+
return cloneState(this.state);
|
|
97
|
+
}
|
|
98
|
+
clear(reason = "manual") {
|
|
99
|
+
if (!this.state) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
logger.info(`[InteractionManager] Cleared interaction: reason=${reason}, kind=${this.state.kind}, expectedInput=${this.state.expectedInput}`);
|
|
103
|
+
this.state = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export const interactionManager = new InteractionManager();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -51,6 +51,12 @@ class PermissionManager {
|
|
|
51
51
|
setMessageId(messageId) {
|
|
52
52
|
this.state.messageId = messageId;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if callback message ID belongs to active permission request
|
|
56
|
+
*/
|
|
57
|
+
isActiveMessage(messageId) {
|
|
58
|
+
return (this.state.isActive && this.state.messageId !== null && messageId === this.state.messageId);
|
|
59
|
+
}
|
|
54
60
|
/**
|
|
55
61
|
* Get Telegram message ID
|
|
56
62
|
*/
|
package/dist/question/manager.js
CHANGED
|
@@ -5,6 +5,8 @@ class QuestionManager {
|
|
|
5
5
|
currentIndex: 0,
|
|
6
6
|
selectedOptions: new Map(),
|
|
7
7
|
customAnswers: new Map(),
|
|
8
|
+
customInputQuestionIndex: null,
|
|
9
|
+
activeMessageId: null,
|
|
8
10
|
messageIds: [],
|
|
9
11
|
isActive: false,
|
|
10
12
|
requestID: null,
|
|
@@ -22,6 +24,8 @@ class QuestionManager {
|
|
|
22
24
|
currentIndex: 0,
|
|
23
25
|
selectedOptions: new Map(),
|
|
24
26
|
customAnswers: new Map(),
|
|
27
|
+
customInputQuestionIndex: null,
|
|
28
|
+
activeMessageId: null,
|
|
25
29
|
messageIds: [],
|
|
26
30
|
isActive: true,
|
|
27
31
|
requestID,
|
|
@@ -87,6 +91,8 @@ class QuestionManager {
|
|
|
87
91
|
}
|
|
88
92
|
nextQuestion() {
|
|
89
93
|
this.state.currentIndex++;
|
|
94
|
+
this.state.customInputQuestionIndex = null;
|
|
95
|
+
this.state.activeMessageId = null;
|
|
90
96
|
logger.debug(`[QuestionManager] Moving to next question: ${this.state.currentIndex}/${this.state.questions.length}`);
|
|
91
97
|
}
|
|
92
98
|
hasNextQuestion() {
|
|
@@ -101,6 +107,29 @@ class QuestionManager {
|
|
|
101
107
|
addMessageId(messageId) {
|
|
102
108
|
this.state.messageIds.push(messageId);
|
|
103
109
|
}
|
|
110
|
+
setActiveMessageId(messageId) {
|
|
111
|
+
this.state.activeMessageId = messageId;
|
|
112
|
+
}
|
|
113
|
+
getActiveMessageId() {
|
|
114
|
+
return this.state.activeMessageId;
|
|
115
|
+
}
|
|
116
|
+
isActiveMessage(messageId) {
|
|
117
|
+
return (this.state.isActive &&
|
|
118
|
+
this.state.activeMessageId !== null &&
|
|
119
|
+
messageId === this.state.activeMessageId);
|
|
120
|
+
}
|
|
121
|
+
startCustomInput(questionIndex) {
|
|
122
|
+
if (!this.state.isActive || !this.state.questions[questionIndex]) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.state.customInputQuestionIndex = questionIndex;
|
|
126
|
+
}
|
|
127
|
+
clearCustomInput() {
|
|
128
|
+
this.state.customInputQuestionIndex = null;
|
|
129
|
+
}
|
|
130
|
+
isWaitingForCustomInput(questionIndex) {
|
|
131
|
+
return this.state.customInputQuestionIndex === questionIndex;
|
|
132
|
+
}
|
|
104
133
|
getMessageIds() {
|
|
105
134
|
return [...this.state.messageIds];
|
|
106
135
|
}
|
|
@@ -111,6 +140,8 @@ class QuestionManager {
|
|
|
111
140
|
cancel() {
|
|
112
141
|
logger.info("[QuestionManager] Poll cancelled");
|
|
113
142
|
this.state.isActive = false;
|
|
143
|
+
this.state.customInputQuestionIndex = null;
|
|
144
|
+
this.state.activeMessageId = null;
|
|
114
145
|
}
|
|
115
146
|
clear() {
|
|
116
147
|
this.state = {
|
|
@@ -118,6 +149,8 @@ class QuestionManager {
|
|
|
118
149
|
currentIndex: 0,
|
|
119
150
|
selectedOptions: new Map(),
|
|
120
151
|
customAnswers: new Map(),
|
|
152
|
+
customInputQuestionIndex: null,
|
|
153
|
+
activeMessageId: null,
|
|
121
154
|
messageIds: [],
|
|
122
155
|
isActive: false,
|
|
123
156
|
requestID: null,
|
package/dist/rename/manager.js
CHANGED
|
@@ -23,6 +23,9 @@ class RenameManager {
|
|
|
23
23
|
getMessageId() {
|
|
24
24
|
return this.state.messageId;
|
|
25
25
|
}
|
|
26
|
+
isActiveMessage(messageId) {
|
|
27
|
+
return (this.state.isWaiting && this.state.messageId !== null && this.state.messageId === messageId);
|
|
28
|
+
}
|
|
26
29
|
isWaitingForName() {
|
|
27
30
|
return this.state.isWaiting;
|
|
28
31
|
}
|