✨ 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 для разработки в не продакшн среде
This commit is contained in:
@@ -20,7 +20,6 @@
|
|||||||
| role_id | uuid FK → role | роль пользователя |
|
| role_id | uuid FK → role | роль пользователя |
|
||||||
| tariff_id | uuid FK → tariff | текущий тариф |
|
| tariff_id | uuid FK → tariff | текущий тариф |
|
||||||
| payment_id | uuid FK → payment | способ оплаты |
|
| payment_id | uuid FK → payment | способ оплаты |
|
||||||
| active_chat_id | uuid FK → chat | активный чат (один пользователь — один чат одновременно) |
|
|
||||||
| fcm_token | string | токен для push-уведомлений (FCM) |
|
| fcm_token | string | токен для push-уведомлений (FCM) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -40,6 +39,7 @@
|
|||||||
| nation | string | национальность |
|
| nation | string | национальность |
|
||||||
| height | float | опционально |
|
| height | float | опционально |
|
||||||
| weight | float | опционально |
|
| weight | float | опционально |
|
||||||
|
| active_chat_id | uuid | ссылка на активный чат (один профиль — один чат одновременно) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { TagsModule } from './modules/tags/tags.module';
|
|||||||
import { CitiesModule } from './modules/cities/cities.module';
|
import { CitiesModule } from './modules/cities/cities.module';
|
||||||
import { GreetingsModule } from './modules/greetings/greetings.module';
|
import { GreetingsModule } from './modules/greetings/greetings.module';
|
||||||
import { GatewaysModule } from './gateways/gateways.module';
|
import { GatewaysModule } from './gateways/gateways.module';
|
||||||
|
import { DevModule } from './modules/dev/dev.module';
|
||||||
|
|
||||||
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||||
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
|
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
|
||||||
@@ -57,6 +58,7 @@ import { TransformInterceptor } from './common/interceptors/transform.intercepto
|
|||||||
CitiesModule,
|
CitiesModule,
|
||||||
GreetingsModule,
|
GreetingsModule,
|
||||||
GatewaysModule,
|
GatewaysModule,
|
||||||
|
...(process.env.NODE_ENV !== 'production' ? [DevModule] : []),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { ChatService } from './chat.service';
|
import { ChatService } from './chat.service';
|
||||||
import { CreateChatDto } from './dto/create-chat.dto';
|
import { CreateChatDto } from './dto/create-chat.dto';
|
||||||
|
import { CloseChatDto } from './dto/close-chat.dto';
|
||||||
import { SendMessageDto } from './dto/send-message.dto';
|
import { SendMessageDto } from './dto/send-message.dto';
|
||||||
import { ChatDto, MessageDto } from './dto/chat-response.dto';
|
import { ChatDto, MessageDto } from './dto/chat-response.dto';
|
||||||
import { MessageResponseDto } from '../../common/dto/message-response.dto';
|
import { MessageResponseDto } from '../../common/dto/message-response.dto';
|
||||||
@@ -60,13 +61,14 @@ export class ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':chatId')
|
@Delete(':chatId')
|
||||||
@ApiOperation({ summary: 'Close a chat' })
|
@ApiOperation({ summary: 'Close a chat (cancel match or report)' })
|
||||||
@ApiOkResponse({ type: MessageResponseDto })
|
@ApiOkResponse({ type: MessageResponseDto })
|
||||||
closeChat(
|
closeChat(
|
||||||
@CurrentUser('id') userId: string,
|
@CurrentUser('id') userId: string,
|
||||||
@Query('profileId') profileId: string,
|
@Query('profileId') profileId: string,
|
||||||
@Param('chatId') chatId: string,
|
@Param('chatId') chatId: string,
|
||||||
|
@Body() dto: CloseChatDto,
|
||||||
) {
|
) {
|
||||||
return this.chatService.closeChat(userId, profileId, chatId);
|
return this.chatService.closeChat(userId, profileId, chatId, dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { and, eq, or } from 'drizzle-orm';
|
import { and, eq, or } from 'drizzle-orm';
|
||||||
import { DrizzleService } from '../../database/drizzle.service';
|
import { DrizzleService } from '../../database/drizzle.service';
|
||||||
import { chat, match, message, profile, user } from '../../database/schema';
|
import { chat, match, message, profile, report, user } from '../../database/schema';
|
||||||
import { NotificationsService } from '../../notifications/notifications.service';
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
import { CreateChatDto } from './dto/create-chat.dto';
|
import { CreateChatDto } from './dto/create-chat.dto';
|
||||||
|
import { CloseChatDto, CloseChatType } from './dto/close-chat.dto';
|
||||||
import { SendMessageDto } from './dto/send-message.dto';
|
import { SendMessageDto } from './dto/send-message.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -76,7 +77,7 @@ export class ChatService {
|
|||||||
return newChat;
|
return newChat;
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeChat(userId: string, profileId: string, chatId: string) {
|
async closeChat(userId: string, profileId: string, chatId: string, dto: CloseChatDto) {
|
||||||
await this.assertProfileOwnership(userId, profileId);
|
await this.assertProfileOwnership(userId, profileId);
|
||||||
|
|
||||||
const [foundChat] = await this.drizzleService.db
|
const [foundChat] = await this.drizzleService.db
|
||||||
@@ -90,6 +91,39 @@ export class ChatService {
|
|||||||
throw new ForbiddenException('Not a chat participant');
|
throw new ForbiddenException('Not a chat participant');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const otherProfileId = foundChat.profile1Id === profileId
|
||||||
|
? foundChat.profile2Id
|
||||||
|
: foundChat.profile1Id;
|
||||||
|
|
||||||
|
let messageText: string;
|
||||||
|
if (dto.type === CloseChatType.Report) {
|
||||||
|
if (!dto.reportReason) throw new BadRequestException('reportReason is required for type=report');
|
||||||
|
messageText = dto.reportDescription
|
||||||
|
? `Жалоба: ${dto.reportReason}. ${dto.reportDescription}`
|
||||||
|
: `Жалоба: ${dto.reportReason}`;
|
||||||
|
|
||||||
|
await this.drizzleService.db
|
||||||
|
.insert(report)
|
||||||
|
.values({
|
||||||
|
sourceProfileId: profileId,
|
||||||
|
entityId: otherProfileId,
|
||||||
|
entityType: 'profile',
|
||||||
|
description: messageText,
|
||||||
|
} as any);
|
||||||
|
} else {
|
||||||
|
messageText = dto.cancelMessage?.trim() || 'Диалог завершён';
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.drizzleService.db
|
||||||
|
.insert(message)
|
||||||
|
.values({
|
||||||
|
chatId,
|
||||||
|
profileId,
|
||||||
|
text: messageText,
|
||||||
|
mediaUrl: null,
|
||||||
|
mediaType: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
await this.drizzleService.db
|
await this.drizzleService.db
|
||||||
.update(chat)
|
.update(chat)
|
||||||
.set({ status: 'closed' } as any)
|
.set({ status: 'closed' } as any)
|
||||||
@@ -108,12 +142,7 @@ export class ChatService {
|
|||||||
return this.drizzleService.db
|
return this.drizzleService.db
|
||||||
.select()
|
.select()
|
||||||
.from(chat)
|
.from(chat)
|
||||||
.where(
|
.where(or(eq(chat.profile1Id, profileId), eq(chat.profile2Id, profileId)));
|
||||||
and(
|
|
||||||
or(eq(chat.profile1Id, profileId), eq(chat.profile2Id, profileId)),
|
|
||||||
eq(chat.status, 'active'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChatMessages(userId: string, profileId: string, chatId: string, page = 1, limit = 50) {
|
async getChatMessages(userId: string, profileId: string, chatId: string, page = 1, limit = 50) {
|
||||||
|
|||||||
31
src/modules/chat/dto/close-chat.dto.ts
Normal file
31
src/modules/chat/dto/close-chat.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
export enum CloseChatType {
|
||||||
|
Cancel = 'cancel',
|
||||||
|
Report = 'report',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CloseChatDto {
|
||||||
|
@ApiProperty({ enum: CloseChatType, description: 'cancel — отменить матч, report — пожаловаться' })
|
||||||
|
@IsEnum(CloseChatType)
|
||||||
|
type: CloseChatType;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Сообщение собеседнику при отмене матча' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(500)
|
||||||
|
cancelMessage?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Причина жалобы (preset)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(200)
|
||||||
|
reportReason?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Дополнительное описание жалобы' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(500)
|
||||||
|
reportDescription?: string;
|
||||||
|
}
|
||||||
71
src/modules/dev/dev.controller.ts
Normal file
71
src/modules/dev/dev.controller.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Controller, Delete, Get, Query } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { eq, inArray, or } from 'drizzle-orm';
|
||||||
|
import { Public } from '../../common/decorators/public.decorator';
|
||||||
|
import { DrizzleService } from '../../database/drizzle.service';
|
||||||
|
import { chat, like, match, profile, user } from '../../database/schema';
|
||||||
|
|
||||||
|
@ApiTags('dev')
|
||||||
|
@Controller('dev')
|
||||||
|
export class DevController {
|
||||||
|
constructor(private readonly drizzle: DrizzleService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('profiles')
|
||||||
|
@ApiOperation({ summary: '[DEV ONLY] List all profiles with basic info' })
|
||||||
|
async getAllProfiles() {
|
||||||
|
return this.drizzle.db
|
||||||
|
.select({
|
||||||
|
id: profile.id,
|
||||||
|
name: profile.name,
|
||||||
|
activeChatId: profile.activeChatId,
|
||||||
|
phone: user.phone,
|
||||||
|
})
|
||||||
|
.from(profile)
|
||||||
|
.leftJoin(user, eq(user.id, profile.userId))
|
||||||
|
.orderBy(profile.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Delete('reset-flow')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '[DEV ONLY] Reset like/match/chat flow for given profiles',
|
||||||
|
description: 'Clears likes, matches, chats and resets activeChatId. Never available in production.',
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: 'profileIds', description: 'Comma-separated profile UUIDs' })
|
||||||
|
async resetFlow(@Query('profileIds') profileIds: string) {
|
||||||
|
const ids = profileIds.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
if (!ids.length) return { deleted: { likes: 0, matches: 0, chats: 0 }, reset: 0 };
|
||||||
|
|
||||||
|
const deletedLikes = await this.drizzle.db
|
||||||
|
.delete(like)
|
||||||
|
.where(or(inArray(like.sourceProfileId, ids), inArray(like.targetProfileId, ids)))
|
||||||
|
.returning({ id: like.id });
|
||||||
|
|
||||||
|
const deletedMatches = await this.drizzle.db
|
||||||
|
.delete(match)
|
||||||
|
.where(or(inArray(match.profile1Id, ids), inArray(match.profile2Id, ids)))
|
||||||
|
.returning({ id: match.id });
|
||||||
|
|
||||||
|
// chats cascade-delete messages
|
||||||
|
const deletedChats = await this.drizzle.db
|
||||||
|
.delete(chat)
|
||||||
|
.where(or(inArray(chat.profile1Id, ids), inArray(chat.profile2Id, ids)))
|
||||||
|
.returning({ id: chat.id });
|
||||||
|
|
||||||
|
const updated = await this.drizzle.db
|
||||||
|
.update(profile)
|
||||||
|
.set({ activeChatId: null } as any)
|
||||||
|
.where(inArray(profile.id, ids))
|
||||||
|
.returning({ id: profile.id });
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleted: {
|
||||||
|
likes: deletedLikes.length,
|
||||||
|
matches: deletedMatches.length,
|
||||||
|
chats: deletedChats.length,
|
||||||
|
},
|
||||||
|
reset: updated.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/modules/dev/dev.module.ts
Normal file
7
src/modules/dev/dev.module.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DevController } from './dev.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [DevController],
|
||||||
|
})
|
||||||
|
export class DevModule {}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { and, eq, gte, ilike, inArray, lte, ne, notInArray, or, sql } from 'drizzle-orm';
|
import { and, eq, gte, ilike, inArray, isNull, lte, ne, notInArray, or, sql } from 'drizzle-orm';
|
||||||
import { DrizzleService } from '../../database/drizzle.service';
|
import { DrizzleService } from '../../database/drizzle.service';
|
||||||
import { like, profile, profileMedia, profileTag, tag, user } from '../../database/schema';
|
import { like, profile, profileMedia, profileTag, tag, user } from '../../database/schema';
|
||||||
import { FeedFilterDto } from './dto/feed-filter.dto';
|
import { FeedFilterDto } from './dto/feed-filter.dto';
|
||||||
@@ -24,6 +24,7 @@ export class FeedService {
|
|||||||
const conditions: any[] = [
|
const conditions: any[] = [
|
||||||
ne(profile.id, profileId),
|
ne(profile.id, profileId),
|
||||||
ne(user.status, 'banned'),
|
ne(user.status, 'banned'),
|
||||||
|
isNull(profile.activeChatId),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (interactedIds.length > 0) {
|
if (interactedIds.length > 0) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { ChatDto } from '../../chat/dto/chat-response.dto';
|
||||||
|
|
||||||
export class LikeDto {
|
export class LikeDto {
|
||||||
@ApiProperty() id: string;
|
@ApiProperty() id: string;
|
||||||
@@ -18,4 +19,5 @@ export class MatchDto {
|
|||||||
export class CreateLikeResponseDto {
|
export class CreateLikeResponseDto {
|
||||||
@ApiProperty({ type: LikeDto }) like: LikeDto;
|
@ApiProperty({ type: LikeDto }) like: LikeDto;
|
||||||
@ApiPropertyOptional({ type: MatchDto, nullable: true }) match: MatchDto | null;
|
@ApiPropertyOptional({ type: MatchDto, nullable: true }) match: MatchDto | null;
|
||||||
|
@ApiPropertyOptional({ type: ChatDto, nullable: true }) chat: ChatDto | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { LikesController } from './likes.controller';
|
import { LikesController } from './likes.controller';
|
||||||
import { LikesService } from './likes.service';
|
import { LikesService } from './likes.service';
|
||||||
|
import { GatewaysModule } from '../../gateways/gateways.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [GatewaysModule],
|
||||||
controllers: [LikesController],
|
controllers: [LikesController],
|
||||||
providers: [LikesService],
|
providers: [LikesService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { BadRequestException, ForbiddenException, Injectable, NotFoundException
|
|||||||
import { and, eq, or } from 'drizzle-orm';
|
import { and, eq, or } from 'drizzle-orm';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DrizzleService } from '../../database/drizzle.service';
|
import { DrizzleService } from '../../database/drizzle.service';
|
||||||
import { like, match, profile, user } from '../../database/schema';
|
import { chat, like, match, profile, user } from '../../database/schema';
|
||||||
import { NotificationsService } from '../../notifications/notifications.service';
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
import { RedisService } from '../../redis/redis.service';
|
import { RedisService } from '../../redis/redis.service';
|
||||||
|
import { ChatGateway } from '../../gateways/chat.gateway';
|
||||||
import { CreateLikeDto } from './dto/create-like.dto';
|
import { CreateLikeDto } from './dto/create-like.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -14,6 +15,7 @@ export class LikesService {
|
|||||||
private readonly notificationsService: NotificationsService,
|
private readonly notificationsService: NotificationsService,
|
||||||
private readonly redisService: RedisService,
|
private readonly redisService: RedisService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly chatGateway: ChatGateway,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createLike(userId: string, dto: CreateLikeDto) {
|
async createLike(userId: string, dto: CreateLikeDto) {
|
||||||
@@ -23,11 +25,15 @@ export class LikesService {
|
|||||||
throw new BadRequestException('Cannot like yourself');
|
throw new BadRequestException('Cannot like yourself');
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxMatches = this.configService.get<number>('app.maxMatchesBeforePause');
|
const [sourceProfile] = await this.drizzleService.db
|
||||||
const activeMatchesCount = await this.getMatchesCount(dto.sourceProfileId);
|
.select({ activeChatId: profile.activeChatId })
|
||||||
if (activeMatchesCount >= maxMatches) {
|
.from(profile)
|
||||||
|
.where(eq(profile.id, dto.sourceProfileId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (sourceProfile?.activeChatId) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`Profile has ${activeMatchesCount} matches. Resolve them before searching for new ones.`,
|
'У вас уже есть активный матч. Завершите текущий диалог перед поиском.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +63,7 @@ export class LikesService {
|
|||||||
return this.checkAndCreateMatch(dto.sourceProfileId, dto.targetProfileId, newLike);
|
return this.checkAndCreateMatch(dto.sourceProfileId, dto.targetProfileId, newLike);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { like: newLike, match: null };
|
return { like: newLike, match: null, chat: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMyMatches(userId: string, profileId: string) {
|
async getMyMatches(userId: string, profileId: string) {
|
||||||
@@ -82,7 +88,17 @@ export class LikesService {
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (reverseLike.length === 0) return { like: newLike, match: null };
|
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
|
const existingMatch = await this.drizzleService.db
|
||||||
.select()
|
.select()
|
||||||
@@ -95,27 +111,56 @@ export class LikesService {
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingMatch.length > 0) return { like: newLike, match: existingMatch[0] };
|
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
|
const [newMatch] = await this.drizzleService.db
|
||||||
.insert(match)
|
.insert(match)
|
||||||
.values({ profile1Id: profileId1, profile2Id: profileId2 })
|
.values({ profile1Id: profileId1, profile2Id: profileId2 })
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await this.notifyMatch(profileId1, profileId2, newMatch.id);
|
const [newChat] = await this.drizzleService.db
|
||||||
|
.insert(chat)
|
||||||
|
.values({
|
||||||
|
profile1Id: profileId1,
|
||||||
|
profile2Id: profileId2,
|
||||||
|
status: 'active',
|
||||||
|
} as any)
|
||||||
|
.returning();
|
||||||
|
|
||||||
return { like: newLike, match: newMatch };
|
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 getMatchesCount(profileId: string): Promise<number> {
|
private async notifyMatch(profileId1: string, profileId2: string, matchId: string, chatId: string) {
|
||||||
const matches = await this.drizzleService.db
|
this.chatGateway.emitToProfile(profileId1, 'match_created', {
|
||||||
.select({ id: match.id })
|
matchId,
|
||||||
.from(match)
|
chatId,
|
||||||
.where(or(eq(match.profile1Id, profileId), eq(match.profile2Id, profileId)));
|
partnerProfileId: profileId2,
|
||||||
return matches.length;
|
});
|
||||||
}
|
this.chatGateway.emitToProfile(profileId2, 'match_created', {
|
||||||
|
matchId,
|
||||||
|
chatId,
|
||||||
|
partnerProfileId: profileId1,
|
||||||
|
});
|
||||||
|
|
||||||
private async notifyMatch(profileId1: string, profileId2: string, matchId: string) {
|
|
||||||
const profiles = await this.drizzleService.db
|
const profiles = await this.drizzleService.db
|
||||||
.select({ userId: profile.userId })
|
.select({ userId: profile.userId })
|
||||||
.from(profile)
|
.from(profile)
|
||||||
@@ -131,16 +176,16 @@ export class LikesService {
|
|||||||
if (u?.fcmToken) {
|
if (u?.fcmToken) {
|
||||||
await this.notificationsService.sendPushNotification(
|
await this.notificationsService.sendPushNotification(
|
||||||
u.fcmToken,
|
u.fcmToken,
|
||||||
'New Match!',
|
'Новый матч!',
|
||||||
'You have a new match! Start chatting now.',
|
'У вас новый матч. Начните общение прямо сейчас.',
|
||||||
{ matchId, type: 'match' },
|
{ matchId, chatId, type: 'match_created' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.redisService.publish(
|
await this.redisService.publish(
|
||||||
'match:created',
|
'match:created',
|
||||||
JSON.stringify({ matchId, profileId1, profileId2 }),
|
JSON.stringify({ matchId, chatId, profileId1, profileId2 }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, Delete, Get, Param, Post, Query, Req, UseGuards } from '@nestjs/common';
|
import { Controller, Delete, Get, Param, Post, Query, Req, UseGuards } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiConsumes, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { FastifyRequest } from 'fastify';
|
import { FastifyRequest } from 'fastify';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
@@ -17,6 +17,15 @@ export class MediaController {
|
|||||||
@Post('upload')
|
@Post('upload')
|
||||||
@ApiOperation({ summary: 'Upload photo / video / audio to profile' })
|
@ApiOperation({ summary: 'Upload photo / video / audio to profile' })
|
||||||
@ApiConsumes('multipart/form-data')
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['file'],
|
||||||
|
properties: {
|
||||||
|
file: { type: 'string', format: 'binary' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@ApiCreatedResponse({ type: MediaItemDto })
|
@ApiCreatedResponse({ type: MediaItemDto })
|
||||||
async upload(
|
async upload(
|
||||||
@CurrentUser('id') userId: string,
|
@CurrentUser('id') userId: string,
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ export class StorageService implements OnModuleInit {
|
|||||||
await this.client.makeBucket(this.bucket);
|
await this.client.makeBucket(this.bucket);
|
||||||
this.logger.log(`Bucket "${this.bucket}" created`);
|
this.logger.log(`Bucket "${this.bucket}" created`);
|
||||||
}
|
}
|
||||||
|
await this.client.setBucketPolicy(
|
||||||
|
this.bucket,
|
||||||
|
JSON.stringify({
|
||||||
|
Version: '2012-10-17',
|
||||||
|
Statement: [
|
||||||
|
{
|
||||||
|
Effect: 'Allow',
|
||||||
|
Principal: { AWS: ['*'] },
|
||||||
|
Action: ['s3:GetObject'],
|
||||||
|
Resource: [`arn:aws:s3:::${this.bucket}/*`],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(
|
async uploadFile(
|
||||||
|
|||||||
Reference in New Issue
Block a user