import { BadRequestException, Injectable, ServiceUnavailableException, } from '@nestjs/common'; export type ExternalRoomRow = Record; const FETCH_TIMEOUT_MS = 12_000; @Injectable() export class ChatExternalClient { normalizeHost(host: string): string { return host .trim() .replace(/^wss?:\/\//, '') .replace(/^https?:\/\//, '') .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( host: string, apiKey: string, payload: { name: string; type: string; members: string[]; }, ): Promise<{ room_id: string }> { const base = this.normalizeHost(host); if (!base) throw new BadRequestException('Host سرویس گفتگو خالی است'); const url = `https://${base}/api/rooms/create`; const { ok, status, bodyText, json } = await this.fetchJson(url, { 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, }), }); if (!ok) this.fail(status, bodyText, 'ایجاد اتاق گفتگو'); const data = (json ?? {}) as { room_id?: string; id?: string }; const roomId = String(data.room_id ?? data.id ?? '').trim(); if (!roomId) { throw new ServiceUnavailableException('پاسخ سرویس گفتگو فاقد room_id است'); } return { room_id: roomId }; } async listUserRooms( host: string, apiKey: string, chatUserId: string, ): Promise { const base = this.normalizeHost(host); if (!base) throw new BadRequestException('Host سرویس گفتگو خالی است'); const params = new URLSearchParams({ api_key: apiKey, user_id: chatUserId, }); const url = `https://${base}/api/users/rooms?${params.toString()}`; const { ok, status, bodyText, json } = await this.fetchJson(url, { method: 'GET', headers: { Accept: 'application/json' }, cache: 'no-store', }); // کاربر جدید بدون room — خالی برگردان if (status === 404 || status === 204) return []; if (!ok) this.fail(status, bodyText, 'دریافت لیست گفتگو از سرویس چت'); const result = json; if (Array.isArray(result)) return result as ExternalRoomRow[]; if (result && typeof result === 'object') { const obj = result as Record; 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 []; } async ping(host: string, apiKey: string, chatUserId: string): Promise { try { await this.listUserRooms(host, apiKey, chatUserId); return true; } catch { return false; } } /** تلاش برای افزودن عضو به اتاق روی سرویس خارجی (endpointهای رایج). */ async addRoomMembers( host: string, apiKey: string, roomId: string, members: string[], ): Promise { const base = this.normalizeHost(host); if (!base || !roomId || !members.length) return false; const attempts: Array<{ path: string; body: Record }> = [ { path: '/api/rooms/add_members', body: { api_key: apiKey, room_id: roomId, members }, }, { path: '/api/rooms/members/add', body: { api_key: apiKey, room_id: roomId, members }, }, { path: '/api/rooms/members', body: { api_key: apiKey, room_id: roomId, members, action: 'add' }, }, ]; for (const attempt of attempts) { try { const { ok, status } = await this.fetchJson(`https://${base}${attempt.path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(attempt.body), }); if (ok || status === 204) return true; if (status === 404 || status === 405) continue; } catch { // try next } } return false; } async removeRoomMembers( host: string, apiKey: string, roomId: string, members: string[], ): Promise { const base = this.normalizeHost(host); if (!base || !roomId || !members.length) return false; const attempts: Array<{ path: string; body: Record }> = [ { path: '/api/rooms/remove_members', body: { api_key: apiKey, room_id: roomId, members }, }, { path: '/api/rooms/members/remove', body: { api_key: apiKey, room_id: roomId, members }, }, { path: '/api/rooms/members', body: { api_key: apiKey, room_id: roomId, members, action: 'remove' }, }, ]; for (const attempt of attempts) { try { const { ok, status } = await this.fetchJson(`https://${base}${attempt.path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(attempt.body), }); if (ok || status === 204) return true; if (status === 404 || status === 405) continue; } catch { // try next } } return false; } }