Edit BadGatewayException
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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 = 'بارگذاری گفتگو ناموفق بود.';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user