feat(vpn): монитор актуальности Яндекс Телемост + инструкция про выход из встречи

- INSTRUCTIONS_TEXT: добавлено требование выйти из встречи, а не завершать её
- vpn.db.ts: findAllActiveVpnTasks для поиска активных VPN-задач
- vpn.service.ts: checkVpnRoomsValidity — периодическая проверка комнат, concurrency=5
- vpn-bot.service.ts: findVpnBotConversationId и notifyVpnRoomExpired
- server/index.ts: worker каждые 6 часов, уведомление пользователю о протухшей ссылке
This commit is contained in:
2026-07-17 13:08:24 +03:00
parent 9f1c2e5b45
commit aea0e9c3fe
4 changed files with 195 additions and 2 deletions

View File

@@ -2,7 +2,7 @@ import fs from "fs";
import path from "path";
import { db } from "../db";
import { conversationMessages, conversationMembers, bots, type Bot, type User } from "@shared/schema";
import { eq, and } from "drizzle-orm";
import { eq, and, sql } from "drizzle-orm";
import { eventBus } from "../routes/shared";
import { enrichMessage, getMembersForSSE } from "../routes/messenger.helpers";
import { notificationService } from "../services/notification.service";
@@ -20,7 +20,8 @@ const INSTRUCTIONS_TEXT = `Привет! Для получения корпор
1. Открой Яндекс Телемост (приложение или https://telemost.yandex.ru).
2. Создай новую видеовстречу.
3. Пришли сюда ссылку на встречу. Примеры того, что подходит:
3. **Важно:** после создания **выйди** из встречи, а не **завершай** её. Тогда ссылка останется активной и будет работать для VPN.
4. Пришли сюда ссылку на встречу. Примеры того, что подходит:
• https://telemost.yandex.ru/j/abc123def456
• telemost.yandex.ru/j/abc123def456
@@ -316,3 +317,46 @@ export async function handleVpnBotCallback({
organizationId,
);
}
// ── Room expiry notifications ─────────────────────────────────────────────────
async function findVpnBotConversationId(
organizationId: number,
userId: number,
): Promise<number | null> {
const botId = await getVpnBotId(organizationId);
if (!botId) return null;
const rows = await db.execute(sql`
SELECT c.id
FROM conversations c
JOIN conversation_members cm ON cm.conversation_id = c.id
WHERE c.type = 'bot_direct'
AND c.bot_id = ${botId}
AND c.organization_id = ${organizationId}
AND cm.user_id = ${userId}
LIMIT 1
`);
interface Row { id: number }
const row = rows.rows[0] as unknown as Row | undefined;
return row?.id ?? null;
}
export async function notifyVpnRoomExpired(
organizationId: number,
userId: number,
roomUrl: string,
): Promise<void> {
const conversationId = await findVpnBotConversationId(organizationId, userId);
if (!conversationId) {
console.warn(`[VPN Bot] no bot_direct conversation found for user ${userId} in org ${organizationId}`);
return;
}
await sendVpnBotMessage(
conversationId,
`⚠️ Ссылка на встречу Яндекс Телемост больше недействительна:\n${roomUrl}\n\nСоздайте новую встречу в Яндекс Телемост (выйдите из неё, не завершая) и пришлите новую ссылку.`,
organizationId,
);
}