godcrm/backend/routes/v3/telegram/commands.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

390 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// backend/routes/v3/telegram/commands.js
// Bot commands: /start, /status, /help, /newchat, /endchat, /chats, /chat_ID
// Admin commands: /adduser, /removeuser, /users
// Auth: /myid, unregistered user handling
import { apiLogger, sendMessage, dbAll, isPostgres, safeJsonParse } from './shared.js';
import { ROOT_ADMIN_ID, userRegistry, saveUserRegistry, isAdmin, getUserInfo, getUserDisplayName } from './userRegistry.js';
import { activeSessions, crmUserIdCache, getCrmUserIdForTelegramUser, resolveAgent, restoreSessionFromDb, loadConversationSession, listRecentTelegramChats, createCrmConversation } from './sessions.js';
/** Handle /myid command. Always works, even for unregistered users. */
export async function handleMyId(chatId, userId, fromUser) {
const userName = fromUser.first_name || fromUser.username || 'User';
await sendMessage(chatId,
`*Your Telegram Info:*\n\n` +
`*ID:* \`${userId}\`\n` +
`*Name:* ${userName}\n` +
`*Username:* ${fromUser.username ? '@' + fromUser.username : '—'}\n` +
`*Chat ID:* \`${chatId}\`\n\n` +
'_Send this ID to the admin so they can add you with_ `/adduser`'
);
}
/** Handle unregistered user access attempt. */
export async function handleUnregisteredUser(chatId, userId, fromUser) {
const userName = fromUser.first_name || fromUser.username || 'User';
apiLogger.warn({ userId, userName, username: fromUser.username }, '[Telegram] Unregistered user');
await sendMessage(chatId,
`👋 Привет, *${userName}*!\n\n` +
'Этот бот приватный. Твой Telegram ID:\n\n' +
`\`${userId}\`\n\n` +
'Отправь этот ID администратору, чтобы он добавил тебя командой:\n' +
`\`/adduser ${userId} ${userName} partner\`\n\n` +
'_Или используй_ `/myid` _чтобы увидеть свой ID._'
);
// Also notify admin about the attempt
const adminChatId = ROOT_ADMIN_ID;
if (adminChatId !== chatId) {
await sendMessage(adminChatId,
`🔔 *Новый пользователь стучится в бот:*\n\n` +
`*Имя:* ${userName}\n` +
`*Username:* ${fromUser.username ? '@' + fromUser.username : '—'}\n` +
`*ID:* \`${userId}\`\n\n` +
`Добавить: \`/adduser ${userId} ${userName} partner\``
);
}
}
/** Handle /adduser command. */
export async function handleAddUser(chatId, userId, text) {
if (!isAdmin(userId)) {
await sendMessage(chatId, '⛔ Только админ может добавлять пользователей.');
return;
}
const currentUserName = getUserDisplayName(userId);
const parts = text.split(/\s+/);
if (parts.length < 4) {
await sendMessage(chatId,
'*Использование:*\n' +
'`/adduser <telegram_id> <name> <role>`\n' +
'`/adduser <telegram_id> <name> <role> <crm_user_id>`\n\n' +
'*Роли:* admin, partner, viewer\n\n' +
'*Пример:*\n' +
'`/adduser 123456789 NIKITRON partner`\n' +
'`/adduser 123456789 NIKITRON partner 2`'
);
return;
}
const targetId = parts[1];
const targetName = parts[2];
const targetRole = parts[3].toLowerCase();
const targetCrmUserId = parts[4] ? parseInt(parts[4], 10) : null;
if (!['admin', 'partner', 'viewer'].includes(targetRole)) {
await sendMessage(chatId, '❌ Роль должна быть: admin, partner или viewer');
return;
}
if (!/^\d+$/.test(targetId)) {
await sendMessage(chatId, '❌ Telegram ID должен быть числом.');
return;
}
const existed = userRegistry.has(targetId);
userRegistry.set(targetId, {
name: targetName,
role: targetRole,
crm_user_id: targetCrmUserId,
added_at: new Date().toISOString(),
added_by: currentUserName,
});
saveUserRegistry();
// Clear CRM user cache for this user
crmUserIdCache.delete(targetId);
await sendMessage(chatId,
`✅ Пользователь ${existed ? 'обновлён' : 'добавлен'}!\n\n` +
`*Имя:* ${targetName}\n` +
`*ID:* \`${targetId}\`\n` +
`*Роль:* ${targetRole}\n` +
(targetCrmUserId ? `*CRM User ID:* ${targetCrmUserId}\n` : '') +
'\nТеперь этот пользователь может использовать бот.'
);
apiLogger.info({ targetId, targetName, targetRole, addedBy: currentUserName }, '[Telegram] User added/updated');
}
/** Handle /users command. */
export async function handleUsers(chatId, userId) {
if (!isAdmin(userId)) {
await sendMessage(chatId, '⛔ Только админ может видеть список пользователей.');
return;
}
let msg = '👥 *Авторизованные пользователи:*\n\n';
const roleEmoji = { admin: '👑', partner: '🤝', viewer: '👁' };
for (const [id, info] of userRegistry) {
const emoji = roleEmoji[info.role] || '▪️';
const isRoot = id === ROOT_ADMIN_ID ? ' _(root)_' : '';
msg += `${emoji} *${info.name}*${isRoot}\n`;
msg += ` ID: \`${id}\` | Role: ${info.role}`;
if (info.crm_user_id) msg += ` | CRM: #${info.crm_user_id}`;
msg += '\n\n';
}
msg += `Всего: ${userRegistry.size}\n`;
msg += '`/adduser <id> <name> <role>` — добавить\n';
msg += '`/removeuser <id>` — удалить';
await sendMessage(chatId, msg);
}
/** Handle /removeuser command. */
export async function handleRemoveUser(chatId, userId, text) {
if (!isAdmin(userId)) {
await sendMessage(chatId, '⛔ Только админ может удалять пользователей.');
return;
}
const currentUserName = getUserDisplayName(userId);
const parts = text.split(/\s+/);
if (parts.length < 2) {
await sendMessage(chatId, '*Использование:* `/removeuser <telegram_id>`');
return;
}
const targetId = parts[1];
if (targetId === ROOT_ADMIN_ID) {
await sendMessage(chatId, '❌ Нельзя удалить root-админа.');
return;
}
if (!userRegistry.has(targetId)) {
await sendMessage(chatId, `❌ Пользователь \`${targetId}\` не найден.`);
return;
}
const removedUser = userRegistry.get(targetId);
userRegistry.delete(targetId);
crmUserIdCache.delete(targetId);
saveUserRegistry();
// Clear their session
activeSessions.delete(targetId);
await sendMessage(chatId,
`✅ Пользователь *${removedUser.name}* (\`${targetId}\`) удалён.`
);
apiLogger.info({ targetId, removedName: removedUser.name, removedBy: currentUserName }, '[Telegram] User removed');
}
/** Handle /start command. */
export async function handleStart(chatId, userId) {
const currentUserInfo = getUserInfo(userId);
const currentUserName = getUserDisplayName(userId);
const greeting = `Привет, *${currentUserName}*! (${currentUserInfo?.role || 'user'})`;
let msg = `🤖 *GOD CRM Bot*\n${greeting}\n\n` +
'*Life Pipeline:*\n' +
'`/sprint` — Текущие задачи спринта\n' +
'`/today` — Брифинг на сегодня\n' +
'`/done <id>` — Завершить задачу\n' +
'`/weight <кг>` — Записать вес\n' +
'`/mood <1-10>` — Записать настроение\n' +
'`/week` — Итоги недели\n' +
'`/fortuna` — 🎡 Колесо Фортуны (перерыв) ✅+15 очков\n\n' +
'*Chat Commands:*\n' +
'`/newchat` — Start new chat (with Orchestrator)\n' +
'`/newchat developer` — Start chat with specific agent\n' +
'`/chats` — List recent chats\n' +
'`/chat_72` — Switch to chat #72\n' +
'`/endchat` — Close current chat\n\n' +
'*After /newchat, just type your messages — no / needed!*\n' +
'*Sessions persist across restarts — your chat is always there.*\n\n' +
'*System:*\n' +
'`/status` — System health\n' +
'`/myid` — Your Telegram ID\n' +
'`/help` — This message';
if (isAdmin(userId)) {
msg += '\n\n*Admin:*\n' +
'`/users` — Список пользователей\n' +
'`/adduser <id> <name> <role>` — Добавить\n' +
'`/removeuser <id>` — Удалить';
}
await sendMessage(chatId, msg);
}
/** Handle /status command. */
export async function handleStatus(chatId, userId) {
const currentUserInfo = getUserInfo(userId);
const currentUserName = getUserDisplayName(userId);
const mem = process.memoryUsage();
const uptime = Math.floor(process.uptime());
let session = activeSessions.get(chatId);
// Try to restore from DB if not in memory
if (!session) {
session = await restoreSessionFromDb(chatId);
}
const sessionInfo = session
? `*Active chat:* #${session.conversationId} with ${session.agentName} (\`/chat_${session.conversationId}\`)`
: '*Active chat:* None (use `/chats` to see recent)';
await sendMessage(chatId,
'📊 *System Status*\n\n' +
`*User:* ${currentUserName} (${currentUserInfo?.role || '?'})\n` +
`*Uptime:* ${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m\n` +
`*Memory:* ${Math.round(mem.heapUsed / 1024 / 1024)}MB / ${Math.round(mem.heapTotal / 1024 / 1024)}MB\n` +
`*Registered users:* ${userRegistry.size}\n` +
`${sessionInfo}\n` +
`*Time:* ${new Date().toISOString()}`
);
}
/** Handle /help command. */
export async function handleHelp(chatId, userId) {
let msg = '🤖 *GOD CRM Bot — Help*\n\n' +
'*🎯 Life Pipeline:*\n' +
'`/sprint` — Текущие задачи спринта (по приоритету)\n' +
'`/today` — Утренний брифинг (календарь + задачи + здоровье)\n' +
'`/done <id>` — Завершить задачу по ID\n' +
'`/weight <кг>` — Записать вес (напр. `/weight 75.5`)\n' +
'`/mood <1-10>` — Записать настроение (напр. `/mood 7`)\n' +
'`/week` — Итоги недели (прогресс + статистика)\n' +
'`/fortune` (`/wheel`) — 🎡 Колесо Фортуны — случайная активность для перерыва\n\n' +
'*💬 Chat Mode (recommended):*\n' +
'`/newchat` — Start chat with Orchestrator\n' +
'`/newchat developer` — Start chat with Developer Ralph\n' +
'`/newchat frontend` — Start chat with Frontend Dev\n' +
'`/newchat architect` — Start chat with Architect\n' +
'After `/newchat`, just type normally — all messages go to the agent.\n\n' +
'*📋 Session Management:*\n' +
'`/chats` — List your recent chats\n' +
'`/chat_72` — Switch to chat #72 (any ID)\n' +
'`/endchat` — Close current chat\n' +
'Sessions survive server restarts — your active chat is remembered.\n\n' +
'*⚙️ System:*\n' +
'`/status` — Health + active chat\n' +
'`/myid` — Your Telegram ID\n' +
'`/help` — This message';
if (isAdmin(userId)) {
msg += '\n\n*👑 Admin Commands:*\n' +
'`/users` — Список авторизованных пользователей\n' +
'`/adduser <id> <name> <role>` — Добавить пользователя\n' +
'`/adduser <id> <name> <role> <crm_id>` — С привязкой к CRM\n' +
'`/removeuser <id>` — Удалить пользователя\n' +
'*Роли:* admin, partner, viewer';
}
await sendMessage(chatId, msg);
}
/** Handle /newchat command. */
export async function handleNewChat(chatId, userId, text) {
const currentUserName = getUserDisplayName(userId);
const parts = text.split(/\s+/);
const agentSlug = parts[1] || 'orchestrator'; // default agent
// Resolve agent
const agent = await resolveAgent(agentSlug);
if (!agent) {
await sendMessage(chatId,
`❌ Agent *${agentSlug}* not found.\n\n` +
'Available: orchestrator, developer, frontend, architect, test-runner, frontend-qa'
);
return;
}
// Close existing session if any
const existingSession = activeSessions.get(chatId);
if (existingSession) {
await sendMessage(chatId,
`📋 Previous chat #${existingSession.conversationId} closed.`
);
}
// Create CRM conversation (linked to this Telegram chatId for session restore)
const crmUserId = await getCrmUserIdForTelegramUser(userId);
const title = `Telegram: ${currentUserName}${agent.name}${new Date().toLocaleDateString('ru-RU')}`;
const conversationId = await createCrmConversation(title, crmUserId, agent, chatId);
// Store session
activeSessions.set(chatId, {
conversationId,
agentUserId: agent.userId,
agentName: agent.name,
agentRowId: agent.rowId,
lastPolledMessageId: 0,
createdAt: new Date().toISOString(),
});
await sendMessage(chatId,
`✅ *Chat started* with *${agent.name}*\n\n` +
`Chat #${conversationId} (\`/chat_${conversationId}\`)\n` +
`CRM: https://devcrm.hltrn.cc/chat/${conversationId}\n\n` +
'Now just type your messages — no `/` needed.\n' +
'Use `/endchat` to pause, `/chat_' + conversationId + '` to return later.'
);
apiLogger.info({ chatId, conversationId, agent: agent.name }, '[Telegram] New chat session created');
}
/** Handle /endchat command. */
export async function handleEndChat(chatId) {
const session = activeSessions.get(chatId);
if (!session) {
await sendMessage(chatId, ' No active chat. Use `/newchat` to start one.');
return;
}
const closedConvId = session.conversationId;
const closedAgent = session.agentName;
activeSessions.delete(chatId);
await sendMessage(chatId,
`✅ Chat #${closedConvId} with *${closedAgent}* closed.\n\n` +
`Return anytime: \`/chat_${closedConvId}\`\n` +
'Or `/newchat` to start a new chat, `/chats` to see all.'
);
}
/** Handle /chats command. */
export async function handleChats(chatId) {
const chats = await listRecentTelegramChats(10);
const session = activeSessions.get(chatId);
if (chats.length === 0) {
await sendMessage(chatId, ' No chats yet. Use `/newchat` to start one.');
return;
}
let msg = '📋 *Recent Chats:*\n\n';
for (const chat of chats) {
const isActive = session && session.conversationId === chat.id;
const marker = isActive ? ' ← *active*' : '';
const agent = chat.agent_name || 'Unknown';
const preview = chat.last_message_preview
? chat.last_message_preview.substring(0, 50).replace(/\n/g, ' ')
: '(empty)';
msg += `${isActive ? '▶️' : '💬'} \`/chat_${chat.id}\` — *${agent}* (${chat.message_count} msgs)${marker}\n`;
msg += ` _${preview}_\n\n`;
}
msg += 'Tap a `/chat_ID` to switch.';
await sendMessage(chatId, msg);
}
/** Handle /chat_ID command (switch to existing conversation). */
export async function handleChatSwitch(chatId, targetConvId) {
const session = await loadConversationSession(chatId, targetConvId);
if (!session) {
await sendMessage(chatId,
`❌ Chat #${targetConvId} not found.\n\nUse \`/chats\` to see available chats.`
);
return;
}
await sendMessage(chatId,
`✅ *Switched to Chat #${session.conversationId}*\n\n` +
`*Agent:* ${session.agentName}\n` +
`CRM: https://devcrm.hltrn.cc/chat/${session.conversationId}\n\n` +
'Now just type your messages — they go to this chat.'
);
}