perf(vpn-bot): фоновая подготовка подписки, оптимизация запросов, анимация кнопок

- validateTelemostRoom: таймаут 3 сек + кэш 10 мин
- createVpnTask: транзакция + batch insert field values
- prepareVpnSubscription: валидация и создание подписки в фоне при получении ссылки
- sendVpnBotMessage: кэш botId, Promise.all для enrich/members/orgMap
- processPlatformSelection: одно сообщение с файлом и подпиской вместо двух
- Chat.tsx: нажатая кнопка становится зелёной галочкой, остальные disabled
This commit is contained in:
2026-07-17 12:38:33 +03:00
parent 6bb89b4cf8
commit 9f1c2e5b45
4 changed files with 224 additions and 103 deletions

View File

@@ -146,7 +146,7 @@ function groupButtonsByRow(buttons: BotButton[]): BotButton[][] {
function MessageBubble({ function MessageBubble({
msg, isOwn, onReply, onEdit, onDelete, reactions, poll, onReactionsChange, onPollChange, msg, isOwn, onReply, onEdit, onDelete, reactions, poll, onReactionsChange, onPollChange,
onOpenUserChat, onOpenBotChat, onButtonClick, onOpenUserChat, onOpenBotChat, onButtonClick, pressedButtons,
}: { }: {
msg: ConvMessage; msg: ConvMessage;
isOwn: boolean; isOwn: boolean;
@@ -160,6 +160,7 @@ function MessageBubble({
onOpenUserChat?: (userId: number) => void; onOpenUserChat?: (userId: number) => void;
onOpenBotChat?: (botId: number) => void; onOpenBotChat?: (botId: number) => void;
onButtonClick?: (m: ConvMessage, button: BotButton) => void; onButtonClick?: (m: ConvMessage, button: BotButton) => void;
pressedButtons?: Set<string>;
}) { }) {
const [pickerOpen, setPickerOpen] = useState(false); const [pickerOpen, setPickerOpen] = useState(false);
const isEdited = msg.updatedAt && msg.updatedAt !== msg.createdAt; const isEdited = msg.updatedAt && msg.updatedAt !== msg.createdAt;
@@ -313,17 +314,26 @@ function MessageBubble({
<div className={cn("flex flex-col gap-1.5 mt-1.5 max-w-full", isOwn ? "items-end" : "items-start")}> <div className={cn("flex flex-col gap-1.5 mt-1.5 max-w-full", isOwn ? "items-end" : "items-start")}>
{groupButtonsByRow(msg.botButtons).map((row, rowIdx) => ( {groupButtonsByRow(msg.botButtons).map((row, rowIdx) => (
<div key={rowIdx} className="flex flex-wrap gap-1.5"> <div key={rowIdx} className="flex flex-wrap gap-1.5">
{row.map((btn, btnIdx) => ( {row.map((btn, btnIdx) => {
const btnKey = `${msg.id}:${btn.callbackData}`;
const isPressed = pressedButtons?.has(btnKey) ?? false;
return (
<Button <Button
key={btnIdx} key={btnIdx}
variant={btn.style === 'primary' ? 'default' : btn.style === 'destructive' ? 'destructive' : btn.style === 'outline' ? 'outline' : 'secondary'} variant={isPressed ? 'default' : btn.style === 'primary' ? 'default' : btn.style === 'destructive' ? 'destructive' : btn.style === 'outline' ? 'outline' : 'secondary'}
size="sm" size="sm"
className="h-7 text-xs px-3 py-1 rounded-full" className={cn(
"h-7 text-xs px-3 py-1 rounded-full",
isPressed && "bg-green-600 hover:bg-green-600 text-white"
)}
disabled={isPressed}
onClick={() => onButtonClick(msg, btn)} onClick={() => onButtonClick(msg, btn)}
> >
{isPressed && <Check className="w-3 h-3 mr-1" />}
{btn.text} {btn.text}
</Button> </Button>
))} );
})}
</div> </div>
))} ))}
</div> </div>
@@ -1000,6 +1010,8 @@ export default function ChatPage() {
// Typing indicator for bots // Typing indicator for bots
const [typingBots, setTypingBots] = useState<Set<string>>(new Set()); const [typingBots, setTypingBots] = useState<Set<string>>(new Set());
const typingTimersRef = useRef<Map<string, NodeJS.Timeout>>(new Map()); const typingTimersRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
// Pressed bot buttons (messageId:callbackData)
const [pressedButtons, setPressedButtons] = useState<Set<string>>(new Set());
// File attachments // File attachments
const [pendingFiles, setPendingFiles] = useState<File[]>([]); const [pendingFiles, setPendingFiles] = useState<File[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -1406,10 +1418,11 @@ export default function ChatPage() {
}; };
useEffect(() => { useEffect(() => {
// Clear all typing indicators when switching conversations // Clear all typing indicators and pressed buttons when switching conversations
typingTimersRef.current.forEach(timer => clearTimeout(timer)); typingTimersRef.current.forEach(timer => clearTimeout(timer));
typingTimersRef.current.clear(); typingTimersRef.current.clear();
setTypingBots(new Set()); setTypingBots(new Set());
setPressedButtons(new Set());
}, [activeConvId]); }, [activeConvId]);
// ── SSE ─────────────────────────────────────────────────────────────────────── // ── SSE ───────────────────────────────────────────────────────────────────────
@@ -2402,8 +2415,16 @@ export default function ChatPage() {
openBotDirectMutation.mutate(botId); openBotDirectMutation.mutate(botId);
}} }}
onButtonClick={(m, btn) => { onButtonClick={(m, btn) => {
const btnKey = `${m.id}:${btn.callbackData}`;
setPressedButtons(prev => {
if (prev.has(btnKey)) return prev;
const next = new Set(prev);
next.add(btnKey);
return next;
});
botButtonCallbackMutation.mutate({ messageId: m.id, callbackData: btn.callbackData }); botButtonCallbackMutation.mutate({ messageId: m.id, callbackData: btn.callbackData });
}} }}
pressedButtons={pressedButtons}
/> />
))} ))}
</div> </div>

View File

@@ -1,12 +1,12 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { db } from "../db"; import { db } from "../db";
import { conversationMessages, conversationMembers, users, bots, type Bot, type User } from "@shared/schema"; import { conversationMessages, conversationMembers, bots, type Bot, type User } from "@shared/schema";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { eventBus } from "../routes/shared"; import { eventBus } from "../routes/shared";
import { enrichMessage, getMembersForSSE } from "../routes/messenger.helpers"; import { enrichMessage, getMembersForSSE } from "../routes/messenger.helpers";
import { notificationService } from "../services/notification.service"; import { notificationService } from "../services/notification.service";
import { getOrCreateVpnSubscription, extractRoomUrl } from "./vpn.service"; import { prepareVpnSubscription, extractRoomUrl, type PreparedSubscription } from "./vpn.service";
import { VPN_BOT_LOGIN } from "./vpn.config"; import { VPN_BOT_LOGIN } from "./vpn.config";
const APPS_BASE_URL = process.env.APP_URL const APPS_BASE_URL = process.env.APP_URL
@@ -46,6 +46,29 @@ type Attachment = { url: string; name: string; size: number; mimeType?: string }
type BotButton = { text: string; callbackData: string; style?: 'primary' | 'secondary' | 'destructive' | 'outline'; row?: number }; type BotButton = { text: string; callbackData: string; style?: 'primary' | 'secondary' | 'destructive' | 'outline'; row?: number };
// ── Bot ID cache ─────────────────────────────────────────────────────────────
const botIdCache = new Map<number, { id: number | null; expiresAt: number }>();
async function getVpnBotId(organizationId: number): Promise<number | null> {
const cached = botIdCache.get(organizationId);
if (cached && cached.expiresAt > Date.now()) {
return cached.id;
}
const [bot] = await db
.select({ id: bots.id })
.from(bots)
.where(and(eq(bots.organizationId, organizationId), eq(bots.login, VPN_BOT_LOGIN)))
.limit(1);
const id = bot?.id ?? null;
botIdCache.set(organizationId, { id, expiresAt: Date.now() + 5 * 60 * 1000 });
return id;
}
// ── Message sending ───────────────────────────────────────────────────────────
export async function sendVpnBotMessage( export async function sendVpnBotMessage(
conversationId: number, conversationId: number,
text: string, text: string,
@@ -53,12 +76,14 @@ export async function sendVpnBotMessage(
attachments?: Attachment[], attachments?: Attachment[],
buttons?: BotButton[], buttons?: BotButton[],
): Promise<void> { ): Promise<void> {
const botId = await getVpnBotId(organizationId);
const [msg] = await db const [msg] = await db
.insert(conversationMessages) .insert(conversationMessages)
.values({ .values({
conversationId, conversationId,
authorId: null, authorId: null,
botId: (await getVpnBotId(organizationId)) ?? null, botId: botId ?? null,
message: text.trim(), message: text.trim(),
replyToId: null, replyToId: null,
mentionedUserIds: null, mentionedUserIds: null,
@@ -67,8 +92,15 @@ export async function sendVpnBotMessage(
}) })
.returning(); .returning();
const enriched = await enrichMessage(msg.id); // Run independent reads in parallel
const members = await getMembersForSSE(conversationId, organizationId); const [enriched, members, memberMuteRows] = await Promise.all([
enrichMessage(msg.id),
getMembersForSSE(conversationId, organizationId),
db
.select({ userId: conversationMembers.userId, mutedAt: conversationMembers.mutedAt, externalOrgId: conversationMembers.externalOrgId })
.from(conversationMembers)
.where(eq(conversationMembers.conversationId, conversationId)),
]);
for (const { userId: memberId, orgId: memberOrgId } of members) { for (const { userId: memberId, orgId: memberOrgId } of members) {
eventBus.publishEvent({ eventBus.publishEvent({
@@ -79,11 +111,6 @@ export async function sendVpnBotMessage(
}); });
} }
const memberMuteRows = await db
.select({ userId: conversationMembers.userId, mutedAt: conversationMembers.mutedAt, externalOrgId: conversationMembers.externalOrgId })
.from(conversationMembers)
.where(eq(conversationMembers.conversationId, conversationId));
const memberOrgMap = new Map( const memberOrgMap = new Map(
memberMuteRows.map(r => [r.userId, { orgId: r.externalOrgId ?? organizationId, mutedAt: r.mutedAt ?? null }]), memberMuteRows.map(r => [r.userId, { orgId: r.externalOrgId ?? organizationId, mutedAt: r.mutedAt ?? null }]),
); );
@@ -100,26 +127,27 @@ export async function sendVpnBotMessage(
}).catch(err => console.error("[VPN Bot] notify error:", err)); }).catch(err => console.error("[VPN Bot] notify error:", err));
} }
async function getVpnBotId(organizationId: number): Promise<number | null> { // ── Pending VPN session (in-memory) ───────────────────────────────────────────
const [bot] = await db
.select({ id: bots.id }) interface PendingVpnSession {
.from(bots) roomUrl: string;
.where(and(eq(bots.organizationId, organizationId), eq(bots.login, VPN_BOT_LOGIN))) user: User;
.limit(1); organizationId: number;
return bot?.id ?? null; createdAt: number;
promise: Promise<PreparedSubscription | { error: string }>;
} }
// Simple in-memory state for the platform-selection step. const pendingVpnSession = new Map<number, PendingVpnSession>();
// Key = conversationId. TTL cleanup runs every 5 minutes.
const pendingPlatform = new Map<number, { roomUrl: string; createdAt: number }>();
function cleanPending() { function cleanPendingSessions() {
const cutoff = Date.now() - 10 * 60 * 1000; const cutoff = Date.now() - 10 * 60 * 1000;
for (const [key, value] of pendingPlatform.entries()) { for (const [key, value] of pendingVpnSession.entries()) {
if (value.createdAt < cutoff) pendingPlatform.delete(key); if (value.createdAt < cutoff) pendingVpnSession.delete(key);
} }
} }
setInterval(cleanPending, 5 * 60 * 1000); setInterval(cleanPendingSessions, 5 * 60 * 1000);
// ── Helpers ─────────────────────────────────────────────────────────────────
function detectPlatform(text: string): "android" | "ios" | null { function detectPlatform(text: string): "android" | "ios" | null {
const lower = text.toLowerCase(); const lower = text.toLowerCase();
@@ -150,6 +178,8 @@ function getFileAttachment(filename: string): Attachment {
}; };
} }
// ── Direct message handling ───────────────────────────────────────────────────
export async function handleVpnBotDirectMessage({ export async function handleVpnBotDirectMessage({
bot, bot,
conversationId, conversationId,
@@ -176,15 +206,15 @@ export async function handleVpnBotDirectMessage({
} }
// Continue platform-selection dialog (text fallback) // Continue platform-selection dialog (text fallback)
const pending = pendingPlatform.get(conversationId); const pending = pendingVpnSession.get(conversationId);
if (pending) { if (pending) {
const platform = detectPlatform(text); const platform = detectPlatform(text);
if (!platform) { if (!platform) {
await sendVpnBotMessage(conversationId, PLATFORM_PROMPT, organizationId, undefined, PLATFORM_BUTTONS); await sendVpnBotMessage(conversationId, PLATFORM_PROMPT, organizationId, undefined, PLATFORM_BUTTONS);
return; return;
} }
pendingPlatform.delete(conversationId); pendingVpnSession.delete(conversationId);
await processPlatformSelection({ conversationId, user, organizationId, roomUrl: pending.roomUrl, platform }); await processPlatformSelection({ conversationId, organizationId, session: pending, platform });
return; return;
} }
@@ -195,55 +225,60 @@ export async function handleVpnBotDirectMessage({
} }
// Start platform-selection dialog with buttons // Start platform-selection dialog with buttons
pendingPlatform.set(conversationId, { roomUrl, createdAt: Date.now() }); // Run heavy work (validation + subscription creation) in background
const session: PendingVpnSession = {
roomUrl,
user,
organizationId,
createdAt: Date.now(),
promise: prepareVpnSubscription(organizationId, user.id, formatUserName(user), roomUrl)
.catch((err: any) => {
console.error("[VPN Bot] background prepare error:", err);
return { error: err instanceof Error ? err.message : "Произошла ошибка. Попробуйте позже." };
}),
};
pendingVpnSession.set(conversationId, session);
await sendVpnBotMessage(conversationId, PLATFORM_PROMPT, organizationId, undefined, PLATFORM_BUTTONS); await sendVpnBotMessage(conversationId, PLATFORM_PROMPT, organizationId, undefined, PLATFORM_BUTTONS);
} }
// ── Platform selection processing ─────────────────────────────────────────────
async function processPlatformSelection({ async function processPlatformSelection({
conversationId, conversationId,
user,
organizationId, organizationId,
roomUrl, session,
platform, platform,
}: { }: {
conversationId: number; conversationId: number;
user: User;
organizationId: number; organizationId: number;
roomUrl: string; session: PendingVpnSession;
platform: "android" | "ios"; platform: "android" | "ios";
}): Promise<void> { }): Promise<void> {
try { const prepared = await session.promise;
const userName = formatUserName(user);
const result = await getOrCreateVpnSubscription(organizationId, user.id, userName, roomUrl);
if (platform === "android") { if ("error" in prepared) {
await sendVpnBotMessage( await sendVpnBotMessage(conversationId, prepared.error, organizationId);
conversationId, return;
"Установи приложение olcbox из прикреплённого APK, затем добавь подписку по ссылке ниже.",
organizationId,
[getFileAttachment(ANDROID_FILE)],
);
} else {
await sendVpnBotMessage(
conversationId,
`${IOS_WARNING}\n\nПосле установки добавь подписку по ссылке ниже.`,
organizationId,
[getFileAttachment(IOS_FILE)],
);
} }
const subscriptionText = result.isNew const attachment = platform === "android"
? `Подписка готова:\n${result.subscriptionUrl}\n\nTelegram и WhatsApp пойдут через латвийский сервер, российские сайты — напрямую.\nСсылка привязана к первому устройству, на котором её активируют.` ? getFileAttachment(ANDROID_FILE)
: `У вас уже есть подписка для этой комнаты:\n${result.subscriptionUrl}`; : getFileAttachment(IOS_FILE);
await sendVpnBotMessage(conversationId, subscriptionText, organizationId); const subscriptionText = prepared.isNew
} catch (err: any) { ? `Подписка готова:\n${prepared.subscriptionUrl}\n\nTelegram и WhatsApp пойдут через латвийский сервер, российские сайты — напрямую.\nСсылка привязана к первому устройству, на котором её активируют.`
console.error("[VPN Bot] handle platform selection error:", err); : `У вас уже есть подписка для этой комнаты:\n${prepared.subscriptionUrl}`;
const userMessage = err instanceof Error ? err.message : "Произошла ошибка. Попробуйте позже.";
await sendVpnBotMessage(conversationId, userMessage, organizationId); const messageText = platform === "android"
} ? `Установи приложение olcbox из прикреплённого APK, затем добавь подписку по ссылке ниже.\n\n${subscriptionText}`
: `${IOS_WARNING}\n\nПосле установки добавь подписку по ссылке ниже.\n\n${subscriptionText}`;
await sendVpnBotMessage(conversationId, messageText, organizationId, [attachment]);
} }
// ── Button callback handling ──────────────────────────────────────────────────
export async function handleVpnBotCallback({ export async function handleVpnBotCallback({
bot, bot,
conversationId, conversationId,
@@ -260,7 +295,7 @@ export async function handleVpnBotCallback({
organizationId: number; organizationId: number;
}): Promise<void> { }): Promise<void> {
if (callbackData === "platform:android" || callbackData === "platform:ios") { if (callbackData === "platform:android" || callbackData === "platform:ios") {
const pending = pendingPlatform.get(conversationId); const pending = pendingVpnSession.get(conversationId);
if (!pending) { if (!pending) {
await sendVpnBotMessage( await sendVpnBotMessage(
conversationId, conversationId,
@@ -270,8 +305,8 @@ export async function handleVpnBotCallback({
return; return;
} }
const platform = callbackData === "platform:android" ? "android" : "ios"; const platform = callbackData === "platform:android" ? "android" : "ios";
pendingPlatform.delete(conversationId); pendingVpnSession.delete(conversationId);
await processPlatformSelection({ conversationId, user, organizationId, roomUrl: pending.roomUrl, platform }); await processPlatformSelection({ conversationId, organizationId, session: pending, platform });
return; return;
} }

View File

@@ -250,28 +250,34 @@ export async function createVpnTask(
const cache = await getVpnFormCache(input.organizationId); const cache = await getVpnFormCache(input.organizationId);
const statusId = getStatusId(cache, input.statusName); const statusId = getStatusId(cache, input.statusName);
const task = await storage.createTask({ return db.transaction(async (tx) => {
const [task] = await tx
.insert(tasks)
.values({
organizationId: input.organizationId, organizationId: input.organizationId,
formId: VPN_FORM_ID, formId: VPN_FORM_ID,
title: input.title, title: input.title,
currentStatusId: statusId, currentStatusId: statusId,
createdBy: input.createdBy, createdBy: input.createdBy,
completedAt: null, isCompleted: false,
dueDate: null, })
}); .returning();
for (const [code, value] of Object.entries(input.fields)) { const fieldInserts = Object.entries(input.fields)
if (value === undefined || value === null) continue; .filter(([, value]) => value !== undefined && value !== null)
const fieldId = getFieldId(cache, code); .map(([code, value]) => ({
await storage.createTaskFieldValue({
taskId: task.id, taskId: task.id,
fieldId, fieldId: getFieldId(cache, code),
formId: VPN_FORM_ID, formId: VPN_FORM_ID,
value, value: value as any,
}); }));
if (fieldInserts.length > 0) {
await tx.insert(taskFieldValues).values(fieldInserts);
} }
return task; return task;
});
} }
export async function updateVpnTaskFieldMap( export async function updateVpnTaskFieldMap(

View File

@@ -72,7 +72,14 @@ export function extractRoomId(roomUrl: string): string {
return roomUrl.slice(idx + 3); return roomUrl.slice(idx + 3);
} }
const telemostValidationCache = new Map<string, { valid: boolean; expiresAt: number }>();
export async function validateTelemostRoom(roomUrl: string): Promise<boolean> { export async function validateTelemostRoom(roomUrl: string): Promise<boolean> {
const cached = telemostValidationCache.get(roomUrl);
if (cached && cached.expiresAt > Date.now()) {
return cached.valid;
}
try { try {
const encoded = encodeURIComponent(roomUrl); const encoded = encodeURIComponent(roomUrl);
const url = `${VPN_TELEMOST_API_BASE}/conferences/${encoded}/connection?next_gen_media_platform_allowed=true&display_name=VPN-Bot&waiting_room_supported=true`; const url = `${VPN_TELEMOST_API_BASE}/conferences/${encoded}/connection?next_gen_media_platform_allowed=true&display_name=VPN-Bot&waiting_room_supported=true`;
@@ -87,20 +94,49 @@ export async function validateTelemostRoom(roomUrl: string): Promise<boolean> {
Origin: 'https://telemost.yandex.ru', Origin: 'https://telemost.yandex.ru',
Referer: 'https://telemost.yandex.ru/', Referer: 'https://telemost.yandex.ru/',
}, },
signal: AbortSignal.timeout(3000),
}); });
return resp.status === 200; const valid = resp.status === 200;
telemostValidationCache.set(roomUrl, { valid, expiresAt: Date.now() + 10 * 60 * 1000 });
return valid;
} catch (err) { } catch (err) {
console.error('[VPN] Telemost validation error:', err); console.error('[VPN] Telemost validation error:', err);
telemostValidationCache.set(roomUrl, { valid: false, expiresAt: Date.now() + 60 * 1000 });
return false; return false;
} }
} }
export async function getOrCreateVpnSubscription( export interface PreparedSubscription {
subscriptionUrl: string;
isNew: boolean;
taskId?: number;
roomId: string;
cryptoKey?: string;
}
export async function getExistingVpnSubscription(
organizationId: number,
roomUrl: string,
): Promise<PreparedSubscription | null> {
const existing = await findVpnTaskByRoomUrl(roomUrl, organizationId);
if (!existing) return null;
const token = existing.values.subscription_token as string | undefined;
if (!token) return null;
return {
subscriptionUrl: buildSubscriptionUrl(token),
isNew: false,
taskId: existing.task.id,
roomId: extractRoomId(roomUrl),
cryptoKey: existing.values.crypto_key as string | undefined,
};
}
export async function prepareVpnSubscription(
organizationId: number, organizationId: number,
userId: number, userId: number,
userName: string, userName: string,
roomUrl: string, roomUrl: string,
): Promise<{ subscriptionUrl: string; isNew: boolean }> { ): Promise<PreparedSubscription> {
const existing = await findVpnTaskByRoomUrl(roomUrl, organizationId); const existing = await findVpnTaskByRoomUrl(roomUrl, organizationId);
if (existing && existing.task.createdBy !== userId) { if (existing && existing.task.createdBy !== userId) {
throw new VpnError('Эта комната уже используется другим сотрудником', 'ROOM_IN_USE'); throw new VpnError('Эта комната уже используется другим сотрудником', 'ROOM_IN_USE');
@@ -108,7 +144,13 @@ export async function getOrCreateVpnSubscription(
if (existing) { if (existing) {
const token = existing.values.subscription_token as string | undefined; const token = existing.values.subscription_token as string | undefined;
if (!token) throw new VpnError('Подписка повреждена, обратитесь к администратору', 'BROKEN'); if (!token) throw new VpnError('Подписка повреждена, обратитесь к администратору', 'BROKEN');
return { subscriptionUrl: buildSubscriptionUrl(token), isNew: false }; return {
subscriptionUrl: buildSubscriptionUrl(token),
isNew: false,
taskId: existing.task.id,
roomId: extractRoomId(roomUrl),
cryptoKey: existing.values.crypto_key as string | undefined,
};
} }
const roomId = extractRoomId(roomUrl); const roomId = extractRoomId(roomUrl);
@@ -125,7 +167,7 @@ export async function getOrCreateVpnSubscription(
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + DEFAULT_SUBSCRIPTION_DAYS); expiresAt.setDate(expiresAt.getDate() + DEFAULT_SUBSCRIPTION_DAYS);
await createVpnTask({ const task = await createVpnTask({
organizationId, organizationId,
createdBy: userId, createdBy: userId,
title: `VPN ${userName || `#${userId}`} / ${roomId}`, title: `VPN ${userName || `#${userId}`} / ${roomId}`,
@@ -144,7 +186,24 @@ export async function getOrCreateVpnSubscription(
}, },
}); });
return { subscriptionUrl: buildSubscriptionUrl(token), isNew: true }; return {
subscriptionUrl: buildSubscriptionUrl(token),
isNew: true,
taskId: task.id,
roomId,
cryptoKey,
};
}
/** @deprecated Use prepareVpnSubscription instead */
export async function getOrCreateVpnSubscription(
organizationId: number,
userId: number,
userName: string,
roomUrl: string,
): Promise<{ subscriptionUrl: string; isNew: boolean }> {
const result = await prepareVpnSubscription(organizationId, userId, userName, roomUrl);
return { subscriptionUrl: result.subscriptionUrl, isNew: result.isNew };
} }
export async function fetchSubscription(token: string, hwid: string | undefined): Promise<string> { export async function fetchSubscription(token: string, hwid: string | undefined): Promise<string> {