✨ 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 для разработки в не продакшн среде
203 lines
6.4 KiB
TypeScript
203 lines
6.4 KiB
TypeScript
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');
|
||
}
|
||
}
|