Files
dating-app-backend/src/modules/likes/likes.service.ts
Oscar cd98f04987 feat(database-schema): добавляет ссылку на активный чат в схему профиля пользователя
 feat(likes.service): обновляет логику создания лайков с учетом активного чата

 feat(likes.module): импортирует модуль Gateways для работы с чатами

 feat(feed.service): добавляет условие для фильтрации профилей без активного чата

 feat(storage.service): устанавливает политику доступа к бакету S3 для получения объектов

 feat(likes-response.dto): добавляет поле чата в ответ на лайк

 feat(media.controller): добавляет описание для загрузки медиафайлов

 feat(chat.service): добавляет возможность закрытия чата с отчетом или сообщением

 feat(chat.controller): обновляет метод закрытия чата для обработки отчетов

 feat(app.module): добавляет модуль Dev для разработки в не продакшн среде
2026-06-09 15:39:38 +03:00

203 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { and, eq, or } from 'drizzle-orm';
import { ConfigService } from '@nestjs/config';
import { DrizzleService } from '../../database/drizzle.service';
import { chat, like, match, profile, user } from '../../database/schema';
import { NotificationsService } from '../../notifications/notifications.service';
import { RedisService } from '../../redis/redis.service';
import { ChatGateway } from '../../gateways/chat.gateway';
import { CreateLikeDto } from './dto/create-like.dto';
@Injectable()
export class LikesService {
constructor(
private readonly drizzleService: DrizzleService,
private readonly notificationsService: NotificationsService,
private readonly redisService: RedisService,
private readonly configService: ConfigService,
private readonly chatGateway: ChatGateway,
) {}
async createLike(userId: string, dto: CreateLikeDto) {
await this.assertProfileOwnership(userId, dto.sourceProfileId);
if (dto.sourceProfileId === dto.targetProfileId) {
throw new BadRequestException('Cannot like yourself');
}
const [sourceProfile] = await this.drizzleService.db
.select({ activeChatId: profile.activeChatId })
.from(profile)
.where(eq(profile.id, dto.sourceProfileId))
.limit(1);
if (sourceProfile?.activeChatId) {
throw new BadRequestException(
'У вас уже есть активный матч. Завершите текущий диалог перед поиском.',
);
}
const existing = await this.drizzleService.db
.select()
.from(like)
.where(
and(
eq(like.sourceProfileId, dto.sourceProfileId),
eq(like.targetProfileId, dto.targetProfileId),
),
)
.limit(1);
if (existing.length > 0) throw new BadRequestException('Already reacted to this profile');
const [newLike] = await this.drizzleService.db
.insert(like)
.values({
sourceProfileId: dto.sourceProfileId,
targetProfileId: dto.targetProfileId,
type: dto.type,
})
.returning();
if (dto.type === 'like') {
return this.checkAndCreateMatch(dto.sourceProfileId, dto.targetProfileId, newLike);
}
return { like: newLike, match: null, chat: null };
}
async getMyMatches(userId: string, profileId: string) {
await this.assertProfileOwnership(userId, profileId);
return this.drizzleService.db
.select()
.from(match)
.where(or(eq(match.profile1Id, profileId), eq(match.profile2Id, profileId)))
.orderBy(match.createdAt);
}
private async checkAndCreateMatch(profileId1: string, profileId2: string, newLike: any) {
const reverseLike = await this.drizzleService.db
.select()
.from(like)
.where(
and(
eq(like.sourceProfileId, profileId2),
eq(like.targetProfileId, profileId1),
eq(like.type, 'like'),
),
)
.limit(1);
if (reverseLike.length === 0) return { like: newLike, match: null, chat: null };
const [targetProfile] = await this.drizzleService.db
.select({ activeChatId: profile.activeChatId })
.from(profile)
.where(eq(profile.id, profileId2))
.limit(1);
if (targetProfile?.activeChatId) {
return { like: newLike, match: null, chat: null };
}
const existingMatch = await this.drizzleService.db
.select()
.from(match)
.where(
or(
and(eq(match.profile1Id, profileId1), eq(match.profile2Id, profileId2)),
and(eq(match.profile1Id, profileId2), eq(match.profile2Id, profileId1)),
),
)
.limit(1);
if (existingMatch.length > 0) {
const existingChat = await this.drizzleService.db
.select()
.from(chat)
.where(
or(
and(eq(chat.profile1Id, profileId1), eq(chat.profile2Id, profileId2)),
and(eq(chat.profile1Id, profileId2), eq(chat.profile2Id, profileId1)),
),
)
.limit(1);
return { like: newLike, match: existingMatch[0], chat: existingChat[0] ?? null };
}
const [newMatch] = await this.drizzleService.db
.insert(match)
.values({ profile1Id: profileId1, profile2Id: profileId2 })
.returning();
const [newChat] = await this.drizzleService.db
.insert(chat)
.values({
profile1Id: profileId1,
profile2Id: profileId2,
status: 'active',
} as any)
.returning();
await this.drizzleService.db
.update(profile)
.set({ activeChatId: newChat.id } as any)
.where(or(eq(profile.id, profileId1), eq(profile.id, profileId2)));
await this.notifyMatch(profileId1, profileId2, newMatch.id, newChat.id);
return { like: newLike, match: newMatch, chat: newChat };
}
private async notifyMatch(profileId1: string, profileId2: string, matchId: string, chatId: string) {
this.chatGateway.emitToProfile(profileId1, 'match_created', {
matchId,
chatId,
partnerProfileId: profileId2,
});
this.chatGateway.emitToProfile(profileId2, 'match_created', {
matchId,
chatId,
partnerProfileId: profileId1,
});
const profiles = await this.drizzleService.db
.select({ userId: profile.userId })
.from(profile)
.where(or(eq(profile.id, profileId1), eq(profile.id, profileId2)));
for (const p of profiles) {
const [u] = await this.drizzleService.db
.select({ fcmToken: user.fcmToken })
.from(user)
.where(eq(user.id, p.userId))
.limit(1);
if (u?.fcmToken) {
await this.notificationsService.sendPushNotification(
u.fcmToken,
'Новый матч!',
'У вас новый матч. Начните общение прямо сейчас.',
{ matchId, chatId, type: 'match_created' },
);
}
}
await this.redisService.publish(
'match:created',
JSON.stringify({ matchId, chatId, profileId1, profileId2 }),
);
}
private async assertProfileOwnership(userId: string, profileId: string) {
const [found] = await this.drizzleService.db
.select({ userId: profile.userId })
.from(profile)
.where(eq(profile.id, profileId))
.limit(1);
if (!found) throw new NotFoundException('Profile not found');
if (found.userId !== userId) throw new ForbiddenException('Profile does not belong to you');
}
}