Edit BadGatewayException

This commit is contained in:
2026-09-29 15:27:14 +03:30
parent 2309e2c5ab
commit 032eb70524
3 changed files with 146 additions and 49 deletions
@@ -1,7 +1,13 @@
import { BadGatewayException, Injectable } from '@nestjs/common'; import {
BadRequestException,
Injectable,
ServiceUnavailableException,
} from '@nestjs/common';
export type ExternalRoomRow = Record<string, unknown>; export type ExternalRoomRow = Record<string, unknown>;
const FETCH_TIMEOUT_MS = 12_000;
@Injectable() @Injectable()
export class ChatExternalClient { export class ChatExternalClient {
normalizeHost(host: string): string { normalizeHost(host: string): string {
@@ -12,6 +18,43 @@ export class ChatExternalClient {
.replace(/\/+$/, ''); .replace(/\/+$/, '');
} }
private async fetchJson(
url: string,
init: RequestInit,
): Promise<{ ok: boolean; status: number; bodyText: string; json: unknown }> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetch(url, { ...init, signal: controller.signal });
const bodyText = await response.text().catch(() => '');
let json: unknown = null;
if (bodyText) {
try {
json = JSON.parse(bodyText);
} catch {
json = null;
}
}
return { ok: response.ok, status: response.status, bodyText, json };
} catch (e) {
const aborted = e instanceof Error && e.name === 'AbortError';
throw new ServiceUnavailableException(
aborted
? `سرویس گفتگو پاسخ نداد (timeout ${FETCH_TIMEOUT_MS / 1000}s). Host و شبکه سرور را بررسی کنید.`
: `اتصال به سرویس گفتگو برقرار نشد: ${e instanceof Error ? e.message : 'unknown'}`,
);
} finally {
clearTimeout(timer);
}
}
private fail(status: number, body: string, action: string): never {
const snippet = body.trim().slice(0, 220);
throw new ServiceUnavailableException(
`${action} ناموفق بود (HTTP ${status})${snippet ? `: ${snippet}` : ''}`,
);
}
async createRoom( async createRoom(
host: string, host: string,
apiKey: string, apiKey: string,
@@ -22,8 +65,9 @@ export class ChatExternalClient {
}, },
): Promise<{ room_id: string }> { ): Promise<{ room_id: string }> {
const base = this.normalizeHost(host); const base = this.normalizeHost(host);
if (!base) throw new BadRequestException('Host سرویس گفتگو خالی است');
const url = `https://${base}/api/rooms/create`; const url = `https://${base}/api/rooms/create`;
const response = await fetch(url, { const { ok, status, bodyText, json } = await this.fetchJson(url, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -33,16 +77,11 @@ export class ChatExternalClient {
members: payload.members, members: payload.members,
}), }),
}); });
if (!response.ok) { if (!ok) this.fail(status, bodyText, 'ایجاد اتاق گفتگو');
const body = await response.text().catch(() => ''); const data = (json ?? {}) as { room_id?: string; id?: string };
throw new BadGatewayException(
`سرویس گفتگو room ایجاد نکرد (${response.status})${body ? `: ${body.slice(0, 200)}` : ''}`,
);
}
const data = (await response.json()) as { room_id?: string; id?: string };
const roomId = String(data.room_id ?? data.id ?? '').trim(); const roomId = String(data.room_id ?? data.id ?? '').trim();
if (!roomId) { if (!roomId) {
throw new BadGatewayException('پاسخ سرویس گفتگو فاقد room_id است'); throw new ServiceUnavailableException('پاسخ سرویس گفتگو فاقد room_id است');
} }
return { room_id: roomId }; return { room_id: roomId };
} }
@@ -53,27 +92,31 @@ export class ChatExternalClient {
chatUserId: string, chatUserId: string,
): Promise<ExternalRoomRow[]> { ): Promise<ExternalRoomRow[]> {
const base = this.normalizeHost(host); const base = this.normalizeHost(host);
if (!base) throw new BadRequestException('Host سرویس گفتگو خالی است');
const params = new URLSearchParams({ const params = new URLSearchParams({
api_key: apiKey, api_key: apiKey,
user_id: chatUserId, user_id: chatUserId,
}); });
const url = `https://${base}/api/users/rooms?${params.toString()}`; const url = `https://${base}/api/users/rooms?${params.toString()}`;
const response = await fetch(url, { const { ok, status, bodyText, json } = await this.fetchJson(url, {
method: 'GET', method: 'GET',
headers: { Accept: 'application/json' }, headers: { Accept: 'application/json' },
cache: 'no-store', cache: 'no-store',
}); });
if (!response.ok) {
const body = await response.text().catch(() => ''); // کاربر جدید بدون room — خالی برگردان
throw new BadGatewayException( if (status === 404 || status === 204) return [];
`دریافت لیست گفتگو ناموفق (${response.status})${body ? `: ${body.slice(0, 200)}` : ''}`,
); if (!ok) this.fail(status, bodyText, 'دریافت لیست گفتگو از سرویس چت');
}
const result = await response.json(); const result = json;
if (Array.isArray(result)) return result as ExternalRoomRow[]; if (Array.isArray(result)) return result as ExternalRoomRow[];
if (Array.isArray(result?.data)) return result.data as ExternalRoomRow[]; if (result && typeof result === 'object') {
if (Array.isArray(result?.rooms)) return result.rooms as ExternalRoomRow[]; const obj = result as Record<string, unknown>;
if (Array.isArray(result?.items)) return result.items as ExternalRoomRow[]; if (Array.isArray(obj.data)) return obj.data as ExternalRoomRow[];
if (Array.isArray(obj.rooms)) return obj.rooms as ExternalRoomRow[];
if (Array.isArray(obj.items)) return obj.items as ExternalRoomRow[];
}
return []; return [];
} }
@@ -180,13 +180,39 @@ export class ConversationsService {
await this.requireUse(userId, organizationId, role); await this.requireUse(userId, organizationId, role);
const creds = await this.getCredentials(organizationId); const creds = await this.getCredentials(organizationId);
const chatUserId = buildChatUserId(organizationId, userId); const chatUserId = buildChatUserId(organizationId, userId);
const external = await this.chat.listUserRooms(creds.host, creds.apiKey, chatUserId);
const tracked = await this.prisma.conversationRoom.findMany({ const tracked = await this.prisma.conversationRoom.findMany({
where: { organizationId }, where: { organizationId },
}); });
const trackedByExternal = new Map(tracked.map((t) => [t.externalRoomId, t])); const trackedByExternal = new Map(tracked.map((t) => [t.externalRoomId, t]));
return external.map((row) => this.toRoomDto(row, trackedByExternal, organizationId, userId)); try {
const external = await this.chat.listUserRooms(creds.host, creds.apiKey, chatUserId);
return external.map((row) =>
this.toRoomDto(row, trackedByExternal, organizationId, userId),
);
} catch (e) {
// اگر سرویس چت در دسترس نبود، حداقل roomهای ثبت‌شده در CRM را نشان بده
if (tracked.length) {
return tracked.map((t) => ({
id: t.id,
externalRoomId: t.externalRoomId,
kind: t.kind === 'group' ? 'group' : 'dm',
name: t.name || (t.kind === 'group' ? 'گروه' : 'گفتگو'),
unreadCount: 0,
lastMessagePreview: null,
lastMessageAt: null,
peerUserId:
t.kind === 'dm'
? t.dmUserMinId === userId
? t.dmUserMaxId
: t.dmUserMaxId === userId
? t.dmUserMinId
: null
: null,
}));
}
throw e;
}
} }
async createDm( async createDm(
@@ -366,20 +392,26 @@ export class ConversationsService {
}); });
if (!row) { if (!row) {
throw new BadRequestException( throw new BadRequestException(
'تنظیمات گفتگو برای این محیط انجام نشده است. از بخش تنظیمات پیکربندی کنید.', 'تنظیمات گفتگو برای این محیط انجام نشده است. از سوپرادمین → محیط‌ها → تنظیمات گفتگو پیکربندی کنید.',
);
}
try {
const apiKey = this.crypto.decrypt({
ciphertext: row.apiKeyCipher,
iv: row.apiKeyIv,
tag: row.apiKeyTag,
});
return {
host: this.chat.normalizeHost(row.chatHost),
apiKey,
uploadUrl: row.uploadUrl,
downloadUrl: row.downloadUrl,
};
} catch {
throw new BadRequestException(
'رمزگشایی API Key گفتگو ناموفق بود. INTEGRATIONS_ENCRYPTION_KEY سرور را بررسی کنید یا تنظیمات را دوباره ذخیره کنید.',
); );
} }
const apiKey = this.crypto.decrypt({
ciphertext: row.apiKeyCipher,
iv: row.apiKeyIv,
tag: row.apiKeyTag,
});
return {
host: this.chat.normalizeHost(row.chatHost),
apiKey,
uploadUrl: row.uploadUrl,
downloadUrl: row.downloadUrl,
};
} }
private toRoomDto( private toRoomDto(
+36 -14
View File
@@ -66,27 +66,49 @@ async function bootstrap() {
loading.value = true; loading.value = true;
error.value = ''; error.value = '';
try { try {
const [accessRes, roomsRes, peersRes] = await Promise.all([ const accessRes = await api.get<ConversationAccessDto>('/conversations/access');
api.get<ConversationAccessDto>('/conversations/access'),
api.get<ConversationRoomDto[]>('/conversations/rooms'),
api.get<ConversationPeerDto[]>('/conversations/peers'),
]);
access.value = accessRes.data; access.value = accessRes.data;
if (!accessRes.data.canUse) { if (!accessRes.data.canUse) {
error.value = 'به گفتگوی درون‌سازمانی دسترسی ندارید.'; error.value = 'به گفتگو دسترسی ندارید.';
return; return;
} }
rooms.value = roomsRes.data;
peers.value = peersRes.data; const [roomsOutcome, peersOutcome, sessionOutcome] = await Promise.allSettled([
const sessionRes = await api.get<{ chatUserId: string }>('/conversations/session'); api.get<ConversationRoomDto[]>('/conversations/rooms'),
chatUserId.value = sessionRes.data.chatUserId; api.get<ConversationPeerDto[]>('/conversations/peers'),
api.get<{ chatUserId: string }>('/conversations/session'),
]);
if (peersOutcome.status === 'fulfilled') {
peers.value = peersOutcome.value.data;
}
if (sessionOutcome.status === 'fulfilled') {
chatUserId.value = sessionOutcome.value.data.chatUserId;
}
if (roomsOutcome.status === 'fulfilled') {
rooms.value = roomsOutcome.value.data;
} else {
const e = roomsOutcome.reason;
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg)
? msg.join('، ')
: msg ||
(e.response?.status === 503 || e.response?.status === 502
? 'اتصال به سرویس چت برقرار نشد. Host و API Key را در سوپرادمین بررسی کنید.'
: 'بارگذاری لیست گفتگو ناموفق بود.');
} else {
error.value = 'بارگذاری لیست گفتگو ناموفق بود.';
}
}
} catch (e) { } catch (e) {
if (axios.isAxiosError(e) && e.response?.status === 403) { if (axios.isAxiosError(e) && e.response?.status === 403) {
error.value = 'ماژول گفتگو برای این محیط فعال نیست.'; error.value = 'ماژول گفتگو برای این محیط فعال نیست.';
} else if (axios.isAxiosError(e) && e.response?.status === 400) { } else if (axios.isAxiosError(e)) {
error.value = const msg = (e.response?.data as { message?: string | string[] })?.message;
(e.response.data as { message?: string })?.message ?? error.value = Array.isArray(msg)
'ابتدا تنظیمات گفتگو را تکمیل کنید.'; ? msg.join('، ')
: msg || 'بارگذاری گفتگو ناموفق بود.';
} else { } else {
error.value = 'بارگذاری گفتگو ناموفق بود.'; error.value = 'بارگذاری گفتگو ناموفق بود.';
} }