Add internal org chat (phase 1) with DM, groups, and org settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { BadGatewayException, Injectable } from '@nestjs/common';
|
||||
|
||||
export type ExternalRoomRow = Record<string, unknown>;
|
||||
|
||||
@Injectable()
|
||||
export class ChatExternalClient {
|
||||
normalizeHost(host: string): string {
|
||||
return host
|
||||
.trim()
|
||||
.replace(/^wss?:\/\//, '')
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
async createRoom(
|
||||
host: string,
|
||||
apiKey: string,
|
||||
payload: {
|
||||
name: string;
|
||||
type: string;
|
||||
members: string[];
|
||||
},
|
||||
): Promise<{ room_id: string }> {
|
||||
const base = this.normalizeHost(host);
|
||||
const url = `https://${base}/api/rooms/create`;
|
||||
const response = await fetch(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 (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
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();
|
||||
if (!roomId) {
|
||||
throw new BadGatewayException('پاسخ سرویس گفتگو فاقد room_id است');
|
||||
}
|
||||
return { room_id: roomId };
|
||||
}
|
||||
|
||||
async listUserRooms(
|
||||
host: string,
|
||||
apiKey: string,
|
||||
chatUserId: string,
|
||||
): Promise<ExternalRoomRow[]> {
|
||||
const base = this.normalizeHost(host);
|
||||
const params = new URLSearchParams({
|
||||
api_key: apiKey,
|
||||
user_id: chatUserId,
|
||||
});
|
||||
const url = `https://${base}/api/users/rooms?${params.toString()}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new BadGatewayException(
|
||||
`دریافت لیست گفتگو ناموفق (${response.status})${body ? `: ${body.slice(0, 200)}` : ''}`,
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (Array.isArray(result)) return result as ExternalRoomRow[];
|
||||
if (Array.isArray(result?.data)) return result.data as ExternalRoomRow[];
|
||||
if (Array.isArray(result?.rooms)) return result.rooms as ExternalRoomRow[];
|
||||
if (Array.isArray(result?.items)) return result.items as ExternalRoomRow[];
|
||||
return [];
|
||||
}
|
||||
|
||||
async ping(host: string, apiKey: string, chatUserId: string): Promise<boolean> {
|
||||
try {
|
||||
await this.listUserRooms(host, apiKey, chatUserId);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/** شناسه پایدار کاربر در سرویس چت خارجی (scoped به org) */
|
||||
export function buildChatUserId(organizationId: string, userId: string): string {
|
||||
const o = organizationId.replace(/-/g, '').slice(0, 12);
|
||||
const u = userId.replace(/-/g, '').slice(0, 12);
|
||||
return `m${o}u${u}`;
|
||||
}
|
||||
|
||||
export function dmPairKey(userA: string, userB: string): [string, string] {
|
||||
return userA < userB ? [userA, userB] : [userB, userA];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { RolePermissionKey, UserRole } from '@mento/shared';
|
||||
import { sanitizePermissions } from '../org-roles/role-defaults';
|
||||
|
||||
export type ConversationAccess = {
|
||||
canUse: boolean;
|
||||
canCreateGroup: boolean;
|
||||
};
|
||||
|
||||
export function resolveConversationAccess(
|
||||
membershipRole: UserRole | string,
|
||||
orgRolePermissions: unknown,
|
||||
): ConversationAccess {
|
||||
if (membershipRole === 'owner' || membershipRole === 'admin') {
|
||||
return { canUse: true, canCreateGroup: true };
|
||||
}
|
||||
const permissions = sanitizePermissions(orgRolePermissions) as RolePermissionKey[];
|
||||
return {
|
||||
canUse: permissions.includes('conversations'),
|
||||
canCreateGroup: permissions.includes('conversations_groups'),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Body, Controller, Get, Patch, Post, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { RequireOrgModule } from '../billing/org-module.decorator';
|
||||
import { OrgModuleGuard } from '../billing/org-module.guard';
|
||||
import {
|
||||
CreateConversationDmDto,
|
||||
CreateConversationGroupDto,
|
||||
UpdateConversationSettingsDto,
|
||||
} from './dto';
|
||||
import { ConversationsService } from './conversations.service';
|
||||
|
||||
@Controller('conversations')
|
||||
@UseGuards(JwtAuthGuard, OrgModuleGuard)
|
||||
@RequireOrgModule('conversations')
|
||||
export class ConversationsController {
|
||||
constructor(private readonly conversations: ConversationsService) {}
|
||||
|
||||
@Get('access')
|
||||
access(@CurrentUser() user: { userId: string; organizationId: string; role: string }) {
|
||||
return this.conversations.getAccess(user.userId, user.organizationId, user.role);
|
||||
}
|
||||
|
||||
@Get('settings')
|
||||
settings(@CurrentUser() user: { organizationId: string; role: string }) {
|
||||
return this.conversations.getSettings(user.organizationId);
|
||||
}
|
||||
|
||||
@Patch('settings')
|
||||
updateSettings(
|
||||
@CurrentUser() user: { organizationId: string; role: string },
|
||||
@Body() dto: UpdateConversationSettingsDto,
|
||||
) {
|
||||
return this.conversations.updateSettings(user.organizationId, user.role, dto);
|
||||
}
|
||||
|
||||
@Get('session')
|
||||
session(@CurrentUser() user: { userId: string; organizationId: string; role: string }) {
|
||||
return this.conversations.getSession(user.userId, user.organizationId, user.role);
|
||||
}
|
||||
|
||||
@Get('peers')
|
||||
peers(@CurrentUser() user: { userId: string; organizationId: string; role: string }) {
|
||||
return this.conversations.listPeers(user.userId, user.organizationId, user.role);
|
||||
}
|
||||
|
||||
@Get('rooms')
|
||||
rooms(@CurrentUser() user: { userId: string; organizationId: string; role: string }) {
|
||||
return this.conversations.listRooms(user.userId, user.organizationId, user.role);
|
||||
}
|
||||
|
||||
@Post('rooms/dm')
|
||||
createDm(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: CreateConversationDmDto,
|
||||
) {
|
||||
return this.conversations.createDm(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.peerUserId,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('rooms/group')
|
||||
createGroup(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: CreateConversationGroupDto,
|
||||
) {
|
||||
return this.conversations.createGroup(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.name,
|
||||
dto.memberUserIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BillingModule } from '../billing/billing.module';
|
||||
import { TahesabModule } from '../integrations/tahesab/tahesab.module';
|
||||
import { ChatExternalClient } from './chat-external.client';
|
||||
import { ConversationsController } from './conversations.controller';
|
||||
import { ConversationsService } from './conversations.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, BillingModule, TahesabModule],
|
||||
controllers: [ConversationsController],
|
||||
providers: [ConversationsService, ChatExternalClient],
|
||||
exports: [ConversationsService],
|
||||
})
|
||||
export class ConversationsModule {}
|
||||
@@ -0,0 +1,431 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
ConversationAccessDto,
|
||||
ConversationPeerDto,
|
||||
ConversationRoomDto,
|
||||
ConversationSessionDto,
|
||||
ConversationSettingsDto,
|
||||
PlatformConversationStatsDto,
|
||||
UpdateConversationSettingsRequest,
|
||||
UserRole,
|
||||
} from '@mento/shared';
|
||||
import { canManageSettings } from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { TahesabCryptoService } from '../integrations/tahesab/tahesab-crypto.service';
|
||||
import { ChatExternalClient, type ExternalRoomRow } from './chat-external.client';
|
||||
import { buildChatUserId, dmPairKey } from './chat-user-id.util';
|
||||
import {
|
||||
resolveConversationAccess,
|
||||
type ConversationAccess,
|
||||
} from './conversations-permissions';
|
||||
|
||||
@Injectable()
|
||||
export class ConversationsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly crypto: TahesabCryptoService,
|
||||
private readonly chat: ChatExternalClient,
|
||||
) {}
|
||||
|
||||
async getAccess(userId: string, organizationId: string, role: string): Promise<ConversationAccessDto> {
|
||||
const access = await this.loadAccess(userId, organizationId, role);
|
||||
return access;
|
||||
}
|
||||
|
||||
async getSettings(organizationId: string): Promise<ConversationSettingsDto> {
|
||||
const row = await this.prisma.conversationSettings.findUnique({
|
||||
where: { organizationId },
|
||||
});
|
||||
if (!row) {
|
||||
return {
|
||||
chatHost: '',
|
||||
uploadUrl: '',
|
||||
downloadUrl: '',
|
||||
apiKeySet: false,
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
chatHost: row.chatHost,
|
||||
uploadUrl: row.uploadUrl,
|
||||
downloadUrl: row.downloadUrl,
|
||||
apiKeySet: true,
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async updateSettings(
|
||||
organizationId: string,
|
||||
role: UserRole | string,
|
||||
dto: UpdateConversationSettingsRequest,
|
||||
): Promise<ConversationSettingsDto> {
|
||||
if (!canManageSettings(role)) {
|
||||
throw new ForbiddenException('فقط مدیر میتواند تنظیمات گفتگو را تغییر دهد');
|
||||
}
|
||||
const host = this.chat.normalizeHost(dto.chatHost);
|
||||
if (!host) throw new BadRequestException('آدرس سرویس گفتگو نامعتبر است');
|
||||
|
||||
const existing = await this.prisma.conversationSettings.findUnique({
|
||||
where: { organizationId },
|
||||
});
|
||||
|
||||
let enc = existing
|
||||
? {
|
||||
ciphertext: existing.apiKeyCipher,
|
||||
iv: existing.apiKeyIv,
|
||||
tag: existing.apiKeyTag,
|
||||
}
|
||||
: null;
|
||||
|
||||
const nextKey = dto.apiKey?.trim();
|
||||
if (nextKey) {
|
||||
enc = this.crypto.encrypt(nextKey);
|
||||
} else if (!enc) {
|
||||
throw new BadRequestException('API Key الزامی است');
|
||||
}
|
||||
|
||||
const uploadUrl = dto.uploadUrl?.trim() ?? existing?.uploadUrl ?? '';
|
||||
const downloadUrl = dto.downloadUrl?.trim() ?? existing?.downloadUrl ?? '';
|
||||
|
||||
await this.prisma.conversationSettings.upsert({
|
||||
where: { organizationId },
|
||||
create: {
|
||||
organizationId,
|
||||
chatHost: host,
|
||||
apiKeyCipher: enc!.ciphertext,
|
||||
apiKeyIv: enc!.iv,
|
||||
apiKeyTag: enc!.tag,
|
||||
uploadUrl,
|
||||
downloadUrl,
|
||||
},
|
||||
update: {
|
||||
chatHost: host,
|
||||
apiKeyCipher: enc!.ciphertext,
|
||||
apiKeyIv: enc!.iv,
|
||||
apiKeyTag: enc!.tag,
|
||||
uploadUrl,
|
||||
downloadUrl,
|
||||
},
|
||||
});
|
||||
|
||||
return this.getSettings(organizationId);
|
||||
}
|
||||
|
||||
async getSession(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
): Promise<ConversationSessionDto> {
|
||||
await this.requireUse(userId, organizationId, role);
|
||||
const creds = await this.getCredentials(organizationId);
|
||||
const chatUserId = buildChatUserId(organizationId, userId);
|
||||
const params = new URLSearchParams({
|
||||
api_key: creds.apiKey,
|
||||
user_id: chatUserId,
|
||||
});
|
||||
const host = creds.host;
|
||||
return {
|
||||
chatUserId,
|
||||
wsUrl: `wss://${host}/ws?${params.toString()}`,
|
||||
messagesBaseUrl: `https://${host}`,
|
||||
uploadUrl: creds.uploadUrl,
|
||||
downloadUrl: creds.downloadUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async listPeers(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
): Promise<ConversationPeerDto[]> {
|
||||
await this.requireUse(userId, organizationId, role);
|
||||
const rows = await this.prisma.membership.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
isActive: true,
|
||||
user: { isActive: true },
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
avatarStorageKey: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { user: { fullName: 'asc' } },
|
||||
});
|
||||
return rows
|
||||
.filter((r) => r.user.id !== userId)
|
||||
.map((r) => ({
|
||||
id: r.user.id,
|
||||
fullName: r.user.fullName,
|
||||
hasAvatar: Boolean(r.user.avatarStorageKey),
|
||||
chatUserId: buildChatUserId(organizationId, r.user.id),
|
||||
}));
|
||||
}
|
||||
|
||||
async listRooms(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
): Promise<ConversationRoomDto[]> {
|
||||
await this.requireUse(userId, organizationId, role);
|
||||
const creds = await this.getCredentials(organizationId);
|
||||
const chatUserId = buildChatUserId(organizationId, userId);
|
||||
const external = await this.chat.listUserRooms(creds.host, creds.apiKey, chatUserId);
|
||||
const tracked = await this.prisma.conversationRoom.findMany({
|
||||
where: { organizationId },
|
||||
});
|
||||
const trackedByExternal = new Map(tracked.map((t) => [t.externalRoomId, t]));
|
||||
|
||||
return external.map((row) => this.toRoomDto(row, trackedByExternal, organizationId, userId));
|
||||
}
|
||||
|
||||
async createDm(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
peerUserId: string,
|
||||
): Promise<ConversationRoomDto> {
|
||||
await this.requireUse(userId, organizationId, role);
|
||||
if (peerUserId === userId) {
|
||||
throw new BadRequestException('نمیتوانید با خودتان گفتگو بسازید');
|
||||
}
|
||||
const peer = await this.prisma.membership.findFirst({
|
||||
where: {
|
||||
organizationId,
|
||||
userId: peerUserId,
|
||||
isActive: true,
|
||||
user: { isActive: true },
|
||||
},
|
||||
include: { user: { select: { fullName: true } } },
|
||||
});
|
||||
if (!peer) throw new NotFoundException('همکار پیدا نشد');
|
||||
|
||||
const [minId, maxId] = dmPairKey(userId, peerUserId);
|
||||
const existing = await this.prisma.conversationRoom.findFirst({
|
||||
where: { organizationId, dmUserMinId: minId, dmUserMaxId: maxId },
|
||||
});
|
||||
if (existing) {
|
||||
return {
|
||||
id: existing.id,
|
||||
externalRoomId: existing.externalRoomId,
|
||||
kind: 'dm',
|
||||
name: peer.user.fullName,
|
||||
unreadCount: 0,
|
||||
lastMessagePreview: null,
|
||||
lastMessageAt: null,
|
||||
peerUserId,
|
||||
};
|
||||
}
|
||||
|
||||
const creds = await this.getCredentials(organizationId);
|
||||
const members = [
|
||||
buildChatUserId(organizationId, userId),
|
||||
buildChatUserId(organizationId, peerUserId),
|
||||
];
|
||||
const created = await this.chat.createRoom(creds.host, creds.apiKey, {
|
||||
name: peer.user.fullName,
|
||||
type: 'DIRECT',
|
||||
members,
|
||||
});
|
||||
|
||||
const row = await this.prisma.conversationRoom.create({
|
||||
data: {
|
||||
organizationId,
|
||||
externalRoomId: created.room_id,
|
||||
kind: 'dm',
|
||||
name: peer.user.fullName,
|
||||
dmUserMinId: minId,
|
||||
dmUserMaxId: maxId,
|
||||
createdByUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
externalRoomId: row.externalRoomId,
|
||||
kind: 'dm',
|
||||
name: peer.user.fullName,
|
||||
unreadCount: 0,
|
||||
lastMessagePreview: null,
|
||||
lastMessageAt: null,
|
||||
peerUserId,
|
||||
};
|
||||
}
|
||||
|
||||
async createGroup(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
name: string,
|
||||
memberUserIds: string[],
|
||||
): Promise<ConversationRoomDto> {
|
||||
const access = await this.loadAccess(userId, organizationId, role);
|
||||
if (!access.canCreateGroup) {
|
||||
throw new ForbiddenException('اجازه ساخت گروه گفتگو را ندارید');
|
||||
}
|
||||
|
||||
const uniqueIds = [...new Set(memberUserIds.filter((id) => id && id !== userId))];
|
||||
const memberships = await this.prisma.membership.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
userId: { in: uniqueIds },
|
||||
isActive: true,
|
||||
user: { isActive: true },
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
if (memberships.length !== uniqueIds.length) {
|
||||
throw new BadRequestException('برخی اعضای گروه در این سازمان فعال نیستند');
|
||||
}
|
||||
|
||||
const creds = await this.getCredentials(organizationId);
|
||||
const members = [
|
||||
buildChatUserId(organizationId, userId),
|
||||
...uniqueIds.map((id) => buildChatUserId(organizationId, id)),
|
||||
];
|
||||
const trimmedName = name.trim();
|
||||
const created = await this.chat.createRoom(creds.host, creds.apiKey, {
|
||||
name: trimmedName,
|
||||
type: 'GROUP',
|
||||
members,
|
||||
});
|
||||
|
||||
const row = await this.prisma.conversationRoom.create({
|
||||
data: {
|
||||
organizationId,
|
||||
externalRoomId: created.room_id,
|
||||
kind: 'group',
|
||||
name: trimmedName,
|
||||
createdByUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
externalRoomId: row.externalRoomId,
|
||||
kind: 'group',
|
||||
name: trimmedName,
|
||||
unreadCount: 0,
|
||||
lastMessagePreview: null,
|
||||
lastMessageAt: null,
|
||||
peerUserId: null,
|
||||
};
|
||||
}
|
||||
|
||||
async platformStats(): Promise<PlatformConversationStatsDto> {
|
||||
const [organizationsWithChat, totalRoomsTracked] = await Promise.all([
|
||||
this.prisma.conversationSettings.count(),
|
||||
this.prisma.conversationRoom.count(),
|
||||
]);
|
||||
const totalConfiguredOrgs = await this.prisma.organization.count({
|
||||
where: { isActive: true },
|
||||
});
|
||||
return {
|
||||
organizationsWithChat,
|
||||
totalConfiguredOrgs,
|
||||
totalRoomsTracked,
|
||||
};
|
||||
}
|
||||
|
||||
private async requireUse(userId: string, organizationId: string, role: string) {
|
||||
const access = await this.loadAccess(userId, organizationId, role);
|
||||
if (!access.canUse) {
|
||||
throw new ForbiddenException('به بخش گفتگو دسترسی ندارید');
|
||||
}
|
||||
await this.getCredentials(organizationId);
|
||||
}
|
||||
|
||||
private async loadAccess(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
): Promise<ConversationAccess> {
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId, isActive: true },
|
||||
include: { orgRole: { select: { permissions: true } } },
|
||||
});
|
||||
return resolveConversationAccess(
|
||||
membership?.role ?? role,
|
||||
membership?.orgRole?.permissions ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
private async getCredentials(organizationId: string) {
|
||||
const row = await this.prisma.conversationSettings.findUnique({
|
||||
where: { organizationId },
|
||||
});
|
||||
if (!row) {
|
||||
throw new BadRequestException(
|
||||
'تنظیمات گفتگو برای این محیط انجام نشده است. از بخش تنظیمات پیکربندی کنید.',
|
||||
);
|
||||
}
|
||||
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(
|
||||
row: ExternalRoomRow,
|
||||
trackedByExternal: Map<
|
||||
string,
|
||||
{
|
||||
kind: string;
|
||||
name: string | null;
|
||||
dmUserMinId: string | null;
|
||||
dmUserMaxId: string | null;
|
||||
}
|
||||
>,
|
||||
organizationId: string,
|
||||
currentUserId: string,
|
||||
): ConversationRoomDto {
|
||||
const externalRoomId = String(row.id ?? row.room_id ?? '').trim();
|
||||
const tracked = trackedByExternal.get(externalRoomId);
|
||||
const kind = tracked?.kind === 'group' ? 'group' : tracked?.kind === 'dm' ? 'dm' : 'group';
|
||||
|
||||
const lastMsg = (row.last_message ?? row.lastMessage) as Record<string, unknown> | undefined;
|
||||
const preview = lastMsg
|
||||
? String(lastMsg.content ?? lastMsg.text ?? lastMsg.body ?? '').trim()
|
||||
: '';
|
||||
const lastAtRaw =
|
||||
lastMsg?.created_at ?? lastMsg?.createdAt ?? row.updated_at ?? row.updatedAt ?? null;
|
||||
const unread = Number(row.unread_count ?? row.unreadCount ?? 0);
|
||||
|
||||
let name =
|
||||
String(tracked?.name ?? row.name ?? row.title ?? '').trim() ||
|
||||
(kind === 'group' ? 'گروه' : 'گفتگو');
|
||||
|
||||
let peerUserId: string | null = null;
|
||||
if (tracked?.kind === 'dm' && tracked.dmUserMinId && tracked.dmUserMaxId) {
|
||||
if (tracked.dmUserMinId === currentUserId) peerUserId = tracked.dmUserMaxId;
|
||||
else if (tracked.dmUserMaxId === currentUserId) peerUserId = tracked.dmUserMinId;
|
||||
}
|
||||
|
||||
return {
|
||||
id: externalRoomId,
|
||||
externalRoomId,
|
||||
kind: kind as 'dm' | 'group',
|
||||
name,
|
||||
unreadCount: Number.isFinite(unread) ? Math.max(0, unread) : 0,
|
||||
lastMessagePreview: preview || null,
|
||||
lastMessageAt: lastAtRaw ? String(lastAtRaw) : null,
|
||||
peerUserId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
ArrayMinSize,
|
||||
ArrayMaxSize,
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateConversationSettingsDto {
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(200)
|
||||
chatHost!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MaxLength(500)
|
||||
apiKey?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
uploadUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
downloadUrl?: string;
|
||||
}
|
||||
|
||||
export class CreateConversationDmDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
peerUserId!: string;
|
||||
}
|
||||
|
||||
export class CreateConversationGroupDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
name!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(50)
|
||||
@IsString({ each: true })
|
||||
memberUserIds!: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user