2026-09-29 15:27:14 +03:30
|
|
|
import {
|
|
|
|
|
BadRequestException,
|
|
|
|
|
Injectable,
|
|
|
|
|
ServiceUnavailableException,
|
|
|
|
|
} from '@nestjs/common';
|
2026-09-29 10:34:01 +03:30
|
|
|
|
|
|
|
|
export type ExternalRoomRow = Record<string, unknown>;
|
|
|
|
|
|
2026-09-29 15:27:14 +03:30
|
|
|
const FETCH_TIMEOUT_MS = 12_000;
|
|
|
|
|
|
2026-09-29 10:34:01 +03:30
|
|
|
@Injectable()
|
|
|
|
|
export class ChatExternalClient {
|
|
|
|
|
normalizeHost(host: string): string {
|
|
|
|
|
return host
|
|
|
|
|
.trim()
|
|
|
|
|
.replace(/^wss?:\/\//, '')
|
|
|
|
|
.replace(/^https?:\/\//, '')
|
|
|
|
|
.replace(/\/+$/, '');
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 15:27:14 +03:30
|
|
|
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}` : ''}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 10:34:01 +03:30
|
|
|
async createRoom(
|
|
|
|
|
host: string,
|
|
|
|
|
apiKey: string,
|
|
|
|
|
payload: {
|
|
|
|
|
name: string;
|
|
|
|
|
type: string;
|
|
|
|
|
members: string[];
|
|
|
|
|
},
|
|
|
|
|
): Promise<{ room_id: string }> {
|
|
|
|
|
const base = this.normalizeHost(host);
|
2026-09-29 15:27:14 +03:30
|
|
|
if (!base) throw new BadRequestException('Host سرویس گفتگو خالی است');
|
2026-09-29 10:34:01 +03:30
|
|
|
const url = `https://${base}/api/rooms/create`;
|
2026-09-29 15:27:14 +03:30
|
|
|
const { ok, status, bodyText, json } = await this.fetchJson(url, {
|
2026-09-29 10:34:01 +03:30
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
api_key: apiKey,
|
|
|
|
|
name: payload.name,
|
|
|
|
|
type: payload.type,
|
|
|
|
|
members: payload.members,
|
|
|
|
|
}),
|
|
|
|
|
});
|
2026-09-29 15:27:14 +03:30
|
|
|
if (!ok) this.fail(status, bodyText, 'ایجاد اتاق گفتگو');
|
|
|
|
|
const data = (json ?? {}) as { room_id?: string; id?: string };
|
2026-09-29 10:34:01 +03:30
|
|
|
const roomId = String(data.room_id ?? data.id ?? '').trim();
|
|
|
|
|
if (!roomId) {
|
2026-09-29 15:27:14 +03:30
|
|
|
throw new ServiceUnavailableException('پاسخ سرویس گفتگو فاقد room_id است');
|
2026-09-29 10:34:01 +03:30
|
|
|
}
|
|
|
|
|
return { room_id: roomId };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listUserRooms(
|
|
|
|
|
host: string,
|
|
|
|
|
apiKey: string,
|
|
|
|
|
chatUserId: string,
|
|
|
|
|
): Promise<ExternalRoomRow[]> {
|
|
|
|
|
const base = this.normalizeHost(host);
|
2026-09-29 15:27:14 +03:30
|
|
|
if (!base) throw new BadRequestException('Host سرویس گفتگو خالی است');
|
2026-09-29 10:34:01 +03:30
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
api_key: apiKey,
|
|
|
|
|
user_id: chatUserId,
|
|
|
|
|
});
|
|
|
|
|
const url = `https://${base}/api/users/rooms?${params.toString()}`;
|
2026-09-29 15:27:14 +03:30
|
|
|
const { ok, status, bodyText, json } = await this.fetchJson(url, {
|
2026-09-29 10:34:01 +03:30
|
|
|
method: 'GET',
|
|
|
|
|
headers: { Accept: 'application/json' },
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
});
|
2026-09-29 15:27:14 +03:30
|
|
|
|
|
|
|
|
// کاربر جدید بدون room — خالی برگردان
|
|
|
|
|
if (status === 404 || status === 204) return [];
|
|
|
|
|
|
|
|
|
|
if (!ok) this.fail(status, bodyText, 'دریافت لیست گفتگو از سرویس چت');
|
|
|
|
|
|
|
|
|
|
const result = json;
|
2026-09-29 10:34:01 +03:30
|
|
|
if (Array.isArray(result)) return result as ExternalRoomRow[];
|
2026-09-29 15:27:14 +03:30
|
|
|
if (result && typeof result === 'object') {
|
|
|
|
|
const obj = result as Record<string, unknown>;
|
|
|
|
|
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[];
|
|
|
|
|
}
|
2026-09-29 10:34:01 +03:30
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async ping(host: string, apiKey: string, chatUserId: string): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
await this.listUserRooms(host, apiKey, chatUserId);
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|