friends system
This commit is contained in:
88
src/friends/dto/friend-response.dto.ts
Normal file
88
src/friends/dto/friend-response.dto.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UserBasicDto {
|
||||
@ApiProperty({
|
||||
description: 'User unique identifier',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "User's display name",
|
||||
example: 'John Doe',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "User's username",
|
||||
example: 'johndoe',
|
||||
required: false,
|
||||
})
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "User's profile picture URL",
|
||||
example: 'https://example.com/avatar.jpg',
|
||||
required: false,
|
||||
})
|
||||
picture?: string;
|
||||
}
|
||||
|
||||
export class FriendRequestResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Friend request unique identifier',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Sender information',
|
||||
type: UserBasicDto,
|
||||
})
|
||||
sender: UserBasicDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Receiver information',
|
||||
type: UserBasicDto,
|
||||
})
|
||||
receiver: UserBasicDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Friend request status',
|
||||
enum: ['PENDING', 'ACCEPTED', 'DENIED'],
|
||||
example: 'PENDING',
|
||||
})
|
||||
status: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Friend request creation timestamp',
|
||||
example: '2024-01-01T00:00:00.000Z',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Friend request last update timestamp',
|
||||
example: '2024-01-01T00:00:00.000Z',
|
||||
})
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export class FriendshipResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Friendship unique identifier',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Friend information',
|
||||
type: UserBasicDto,
|
||||
})
|
||||
friend: UserBasicDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Friendship creation timestamp',
|
||||
example: '2024-01-01T00:00:00.000Z',
|
||||
})
|
||||
createdAt: Date;
|
||||
}
|
||||
13
src/friends/dto/search-users.dto.ts
Normal file
13
src/friends/dto/search-users.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class SearchUsersDto {
|
||||
@ApiProperty({
|
||||
description: 'Username to search for (partial match)',
|
||||
example: 'john',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
}
|
||||
13
src/friends/dto/send-friend-request.dto.ts
Normal file
13
src/friends/dto/send-friend-request.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class SendFriendRequestDto {
|
||||
@ApiProperty({
|
||||
description: 'User ID to send friend request to',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@IsUUID()
|
||||
receiverId: string;
|
||||
}
|
||||
298
src/friends/friends.controller.spec.ts
Normal file
298
src/friends/friends.controller.spec.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FriendsController } from './friends.controller';
|
||||
import { FriendsService } from './friends.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { StateGateway } from '../ws/state/state.gateway';
|
||||
|
||||
enum FriendRequestStatus {
|
||||
PENDING = 'PENDING',
|
||||
ACCEPTED = 'ACCEPTED',
|
||||
DENIED = 'DENIED',
|
||||
}
|
||||
|
||||
describe('FriendsController', () => {
|
||||
let controller: FriendsController;
|
||||
|
||||
const mockAuthUser = {
|
||||
keycloakSub: 'f:realm:user1',
|
||||
email: 'user1@example.com',
|
||||
name: 'User One',
|
||||
username: 'user1',
|
||||
};
|
||||
|
||||
const mockUser1 = {
|
||||
id: 'user-1',
|
||||
keycloakSub: 'f:realm:user1',
|
||||
email: 'user1@example.com',
|
||||
name: 'User One',
|
||||
username: 'user1',
|
||||
picture: null,
|
||||
roles: [],
|
||||
lastLoginAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockUser2 = {
|
||||
id: 'user-2',
|
||||
keycloakSub: 'f:realm:user2',
|
||||
email: 'user2@example.com',
|
||||
name: 'User Two',
|
||||
username: 'user2',
|
||||
picture: null,
|
||||
roles: [],
|
||||
lastLoginAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockFriendRequest = {
|
||||
id: 'request-1',
|
||||
senderId: 'user-1',
|
||||
receiverId: 'user-2',
|
||||
status: FriendRequestStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
sender: mockUser1,
|
||||
receiver: mockUser2,
|
||||
};
|
||||
|
||||
const mockFriendship = {
|
||||
id: 'friendship-1',
|
||||
userId: 'user-1',
|
||||
friendId: 'user-2',
|
||||
createdAt: new Date(),
|
||||
friend: mockUser2,
|
||||
};
|
||||
|
||||
const mockFriendsService = {
|
||||
sendFriendRequest: jest.fn(),
|
||||
getPendingReceivedRequests: jest.fn(),
|
||||
getPendingSentRequests: jest.fn(),
|
||||
acceptFriendRequest: jest.fn(),
|
||||
denyFriendRequest: jest.fn(),
|
||||
getFriends: jest.fn(),
|
||||
unfriend: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUsersService = {
|
||||
searchUsers: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAuthService = {
|
||||
syncUserFromToken: jest.fn(),
|
||||
};
|
||||
|
||||
const mockStateGateway = {
|
||||
emitFriendRequestReceived: jest.fn(),
|
||||
emitFriendRequestAccepted: jest.fn(),
|
||||
emitFriendRequestDenied: jest.fn(),
|
||||
emitUnfriended: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [FriendsController],
|
||||
providers: [
|
||||
{ provide: FriendsService, useValue: mockFriendsService },
|
||||
{ provide: UsersService, useValue: mockUsersService },
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: StateGateway, useValue: mockStateGateway },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<FriendsController>(FriendsController);
|
||||
|
||||
jest.clearAllMocks();
|
||||
mockAuthService.syncUserFromToken.mockResolvedValue(mockUser1);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('searchUsers', () => {
|
||||
it('should return list of users matching search', async () => {
|
||||
const users = [mockUser2];
|
||||
mockUsersService.searchUsers.mockResolvedValue(users);
|
||||
|
||||
const result = await controller.searchUsers(
|
||||
{ username: 'user2' },
|
||||
mockAuthUser,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: mockUser2.id,
|
||||
name: mockUser2.name,
|
||||
username: mockUser2.username,
|
||||
picture: undefined,
|
||||
},
|
||||
]);
|
||||
expect(mockUsersService.searchUsers).toHaveBeenCalledWith(
|
||||
'user2',
|
||||
'user-1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendFriendRequest', () => {
|
||||
it('should send friend request and emit WebSocket event', async () => {
|
||||
mockFriendsService.sendFriendRequest.mockResolvedValue(mockFriendRequest);
|
||||
|
||||
const result = await controller.sendFriendRequest(
|
||||
{ receiverId: 'user-2' },
|
||||
mockAuthUser,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: mockFriendRequest.id,
|
||||
sender: {
|
||||
id: mockUser1.id,
|
||||
name: mockUser1.name,
|
||||
username: mockUser1.username,
|
||||
picture: undefined,
|
||||
},
|
||||
receiver: {
|
||||
id: mockUser2.id,
|
||||
name: mockUser2.name,
|
||||
username: mockUser2.username,
|
||||
picture: undefined,
|
||||
},
|
||||
status: FriendRequestStatus.PENDING,
|
||||
createdAt: mockFriendRequest.createdAt,
|
||||
updatedAt: mockFriendRequest.updatedAt,
|
||||
});
|
||||
expect(mockFriendsService.sendFriendRequest).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
'user-2',
|
||||
);
|
||||
expect(mockStateGateway.emitFriendRequestReceived).toHaveBeenCalledWith(
|
||||
'user-2',
|
||||
mockFriendRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReceivedRequests', () => {
|
||||
it('should return list of received friend requests', async () => {
|
||||
mockFriendsService.getPendingReceivedRequests.mockResolvedValue([
|
||||
mockFriendRequest,
|
||||
]);
|
||||
|
||||
const result = await controller.getReceivedRequests(mockAuthUser);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(mockFriendRequest.id);
|
||||
expect(
|
||||
mockFriendsService.getPendingReceivedRequests,
|
||||
).toHaveBeenCalledWith('user-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSentRequests', () => {
|
||||
it('should return list of sent friend requests', async () => {
|
||||
mockFriendsService.getPendingSentRequests.mockResolvedValue([
|
||||
mockFriendRequest,
|
||||
]);
|
||||
|
||||
const result = await controller.getSentRequests(mockAuthUser);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(mockFriendRequest.id);
|
||||
expect(mockFriendsService.getPendingSentRequests).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('acceptFriendRequest', () => {
|
||||
it('should accept friend request and emit WebSocket event', async () => {
|
||||
const acceptedRequest = {
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.ACCEPTED,
|
||||
};
|
||||
mockFriendsService.acceptFriendRequest.mockResolvedValue(acceptedRequest);
|
||||
|
||||
const result = await controller.acceptFriendRequest(
|
||||
'request-1',
|
||||
mockAuthUser,
|
||||
);
|
||||
|
||||
expect(result.status).toBe(FriendRequestStatus.ACCEPTED);
|
||||
expect(mockFriendsService.acceptFriendRequest).toHaveBeenCalledWith(
|
||||
'request-1',
|
||||
'user-1',
|
||||
);
|
||||
expect(mockStateGateway.emitFriendRequestAccepted).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
acceptedRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('denyFriendRequest', () => {
|
||||
it('should deny friend request and emit WebSocket event', async () => {
|
||||
const deniedRequest = {
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.DENIED,
|
||||
};
|
||||
mockFriendsService.denyFriendRequest.mockResolvedValue(deniedRequest);
|
||||
|
||||
const result = await controller.denyFriendRequest(
|
||||
'request-1',
|
||||
mockAuthUser,
|
||||
);
|
||||
|
||||
expect(result.status).toBe(FriendRequestStatus.DENIED);
|
||||
expect(mockFriendsService.denyFriendRequest).toHaveBeenCalledWith(
|
||||
'request-1',
|
||||
'user-1',
|
||||
);
|
||||
expect(mockStateGateway.emitFriendRequestDenied).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
deniedRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFriends', () => {
|
||||
it('should return list of friends', async () => {
|
||||
mockFriendsService.getFriends.mockResolvedValue([mockFriendship]);
|
||||
|
||||
const result = await controller.getFriends(mockAuthUser);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: mockFriendship.id,
|
||||
friend: {
|
||||
id: mockUser2.id,
|
||||
name: mockUser2.name,
|
||||
username: mockUser2.username,
|
||||
picture: undefined,
|
||||
},
|
||||
createdAt: mockFriendship.createdAt,
|
||||
},
|
||||
]);
|
||||
expect(mockFriendsService.getFriends).toHaveBeenCalledWith('user-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unfriend', () => {
|
||||
it('should unfriend user and emit WebSocket event', async () => {
|
||||
mockFriendsService.unfriend.mockResolvedValue(undefined);
|
||||
|
||||
await controller.unfriend('user-2', mockAuthUser);
|
||||
|
||||
expect(mockFriendsService.unfriend).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
'user-2',
|
||||
);
|
||||
expect(mockStateGateway.emitUnfriended).toHaveBeenCalledWith(
|
||||
'user-2',
|
||||
'user-1',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
389
src/friends/friends.controller.ts
Normal file
389
src/friends/friends.controller.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiParam,
|
||||
ApiBearerAuth,
|
||||
ApiUnauthorizedResponse,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { User, FriendRequest } from '@prisma/client';
|
||||
import { FriendsService } from './friends.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import {
|
||||
CurrentUser,
|
||||
type AuthenticatedUser,
|
||||
} from '../auth/decorators/current-user.decorator';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { SendFriendRequestDto } from './dto/send-friend-request.dto';
|
||||
import {
|
||||
FriendRequestResponseDto,
|
||||
FriendshipResponseDto,
|
||||
UserBasicDto,
|
||||
} from './dto/friend-response.dto';
|
||||
import { SearchUsersDto } from './dto/search-users.dto';
|
||||
|
||||
type FriendRequestWithRelations = FriendRequest & {
|
||||
sender: User;
|
||||
receiver: User;
|
||||
};
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { StateGateway } from '../ws/state/state.gateway';
|
||||
|
||||
@ApiTags('friends')
|
||||
@Controller('friends')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class FriendsController {
|
||||
private readonly logger = new Logger(FriendsController.name);
|
||||
|
||||
constructor(
|
||||
private readonly friendsService: FriendsService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly stateGateway: StateGateway,
|
||||
) {}
|
||||
|
||||
@Get('search')
|
||||
@ApiOperation({
|
||||
summary: 'Search users by username',
|
||||
description: 'Search for users by username to send friend requests',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'username',
|
||||
required: false,
|
||||
description: 'Username to search for (partial match)',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of users matching search criteria',
|
||||
type: [UserBasicDto],
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async searchUsers(
|
||||
@Query() searchDto: SearchUsersDto,
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<UserBasicDto[]> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.debug(
|
||||
`Searching users with username: ${searchDto.username || 'all'}`,
|
||||
);
|
||||
|
||||
const users = await this.usersService.searchUsers(
|
||||
searchDto.username,
|
||||
user.id,
|
||||
);
|
||||
|
||||
return users.map((u: User) => ({
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
username: u.username ?? undefined,
|
||||
picture: u.picture ?? undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@Post('requests')
|
||||
@ApiOperation({
|
||||
summary: 'Send a friend request',
|
||||
description: 'Send a friend request to another user',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Friend request sent successfully',
|
||||
type: FriendRequestResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Invalid request (self-request, already friends, etc.)',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'User not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 409,
|
||||
description: 'Friend request already exists',
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async sendFriendRequest(
|
||||
@Body() sendRequestDto: SendFriendRequestDto,
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<FriendRequestResponseDto> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.log(
|
||||
`User ${user.id} sending friend request to ${sendRequestDto.receiverId}`,
|
||||
);
|
||||
|
||||
const friendRequest = await this.friendsService.sendFriendRequest(
|
||||
user.id,
|
||||
sendRequestDto.receiverId,
|
||||
);
|
||||
|
||||
this.stateGateway.emitFriendRequestReceived(
|
||||
sendRequestDto.receiverId,
|
||||
friendRequest,
|
||||
);
|
||||
|
||||
return this.mapFriendRequestToDto(friendRequest);
|
||||
}
|
||||
|
||||
@Get('requests/received')
|
||||
@ApiOperation({
|
||||
summary: 'Get received friend requests',
|
||||
description: 'Get all pending friend requests received by the current user',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of received friend requests',
|
||||
type: [FriendRequestResponseDto],
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async getReceivedRequests(
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<FriendRequestResponseDto[]> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.debug(`Getting received friend requests for user ${user.id}`);
|
||||
|
||||
const requests = await this.friendsService.getPendingReceivedRequests(
|
||||
user.id,
|
||||
);
|
||||
|
||||
return requests.map((req) => this.mapFriendRequestToDto(req));
|
||||
}
|
||||
|
||||
@Get('requests/sent')
|
||||
@ApiOperation({
|
||||
summary: 'Get sent friend requests',
|
||||
description: 'Get all pending friend requests sent by the current user',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of sent friend requests',
|
||||
type: [FriendRequestResponseDto],
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async getSentRequests(
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<FriendRequestResponseDto[]> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.debug(`Getting sent friend requests for user ${user.id}`);
|
||||
|
||||
const requests = await this.friendsService.getPendingSentRequests(user.id);
|
||||
|
||||
return requests.map((req) => this.mapFriendRequestToDto(req));
|
||||
}
|
||||
|
||||
@Post('requests/:id/accept')
|
||||
@ApiOperation({
|
||||
summary: 'Accept a friend request',
|
||||
description: 'Accept a pending friend request',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'Friend request ID',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Friend request accepted successfully',
|
||||
type: FriendRequestResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Invalid request (not receiver, already processed, etc.)',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Friend request not found',
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async acceptFriendRequest(
|
||||
@Param('id') requestId: string,
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<FriendRequestResponseDto> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.log(`User ${user.id} accepting friend request ${requestId}`);
|
||||
|
||||
const friendRequest = await this.friendsService.acceptFriendRequest(
|
||||
requestId,
|
||||
user.id,
|
||||
);
|
||||
|
||||
this.stateGateway.emitFriendRequestAccepted(
|
||||
friendRequest.senderId,
|
||||
friendRequest,
|
||||
);
|
||||
|
||||
return this.mapFriendRequestToDto(friendRequest);
|
||||
}
|
||||
|
||||
@Post('requests/:id/deny')
|
||||
@ApiOperation({
|
||||
summary: 'Deny a friend request',
|
||||
description: 'Deny a pending friend request',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'Friend request ID',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Friend request denied successfully',
|
||||
type: FriendRequestResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Invalid request (not receiver, already processed, etc.)',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Friend request not found',
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async denyFriendRequest(
|
||||
@Param('id') requestId: string,
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<FriendRequestResponseDto> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.log(`User ${user.id} denying friend request ${requestId}`);
|
||||
|
||||
const friendRequest = await this.friendsService.denyFriendRequest(
|
||||
requestId,
|
||||
user.id,
|
||||
);
|
||||
|
||||
this.stateGateway.emitFriendRequestDenied(
|
||||
friendRequest.senderId,
|
||||
friendRequest,
|
||||
);
|
||||
|
||||
return this.mapFriendRequestToDto(friendRequest);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get friends list',
|
||||
description: 'Get all friends of the current user',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of friends',
|
||||
type: [FriendshipResponseDto],
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async getFriends(
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<FriendshipResponseDto[]> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.debug(`Getting friends list for user ${user.id}`);
|
||||
|
||||
const friendships = await this.friendsService.getFriends(user.id);
|
||||
|
||||
return friendships.map((friendship) => ({
|
||||
id: friendship.id,
|
||||
friend: {
|
||||
id: friendship.friend.id,
|
||||
name: friendship.friend.name,
|
||||
username: friendship.friend.username ?? undefined,
|
||||
picture: friendship.friend.picture ?? undefined,
|
||||
},
|
||||
createdAt: friendship.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
@Delete(':friendId')
|
||||
@HttpCode(204)
|
||||
@ApiOperation({
|
||||
summary: 'Unfriend a user',
|
||||
description: 'Remove a user from your friends list',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'friendId',
|
||||
description: 'Friend user ID',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 204,
|
||||
description: 'Successfully unfriended',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Invalid request (cannot unfriend yourself)',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Friend not found',
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Invalid or missing JWT token',
|
||||
})
|
||||
async unfriend(
|
||||
@Param('friendId') friendId: string,
|
||||
@CurrentUser() authUser: AuthenticatedUser,
|
||||
): Promise<void> {
|
||||
const user = await this.authService.syncUserFromToken(authUser);
|
||||
|
||||
this.logger.log(`User ${user.id} unfriending user ${friendId}`);
|
||||
|
||||
await this.friendsService.unfriend(user.id, friendId);
|
||||
|
||||
this.stateGateway.emitUnfriended(friendId, user.id);
|
||||
}
|
||||
|
||||
private mapFriendRequestToDto(
|
||||
friendRequest: FriendRequestWithRelations,
|
||||
): FriendRequestResponseDto {
|
||||
return {
|
||||
id: friendRequest.id,
|
||||
sender: {
|
||||
id: friendRequest.sender.id,
|
||||
name: friendRequest.sender.name,
|
||||
username: friendRequest.sender.username ?? undefined,
|
||||
picture: friendRequest.sender.picture ?? undefined,
|
||||
},
|
||||
receiver: {
|
||||
id: friendRequest.receiver.id,
|
||||
name: friendRequest.receiver.name,
|
||||
username: friendRequest.receiver.username ?? undefined,
|
||||
picture: friendRequest.receiver.picture ?? undefined,
|
||||
},
|
||||
status: friendRequest.status,
|
||||
createdAt: friendRequest.createdAt,
|
||||
updatedAt: friendRequest.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
src/friends/friends.module.ts
Normal file
15
src/friends/friends.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FriendsController } from './friends.controller';
|
||||
import { FriendsService } from './friends.service';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { WsModule } from '../ws/ws.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, AuthModule, UsersModule, WsModule],
|
||||
controllers: [FriendsController],
|
||||
providers: [FriendsService],
|
||||
exports: [FriendsService],
|
||||
})
|
||||
export class FriendsModule {}
|
||||
435
src/friends/friends.service.spec.ts
Normal file
435
src/friends/friends.service.spec.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FriendsService } from './friends.service';
|
||||
import { PrismaService } from '../database/prisma.service';
|
||||
import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
enum FriendRequestStatus {
|
||||
PENDING = 'PENDING',
|
||||
ACCEPTED = 'ACCEPTED',
|
||||
DENIED = 'DENIED',
|
||||
}
|
||||
|
||||
describe('FriendsService', () => {
|
||||
let service: FriendsService;
|
||||
|
||||
const mockUser1 = {
|
||||
id: 'user-1',
|
||||
keycloakSub: 'f:realm:user1',
|
||||
email: 'user1@example.com',
|
||||
name: 'User One',
|
||||
username: 'user1',
|
||||
picture: null,
|
||||
roles: [],
|
||||
lastLoginAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockUser2 = {
|
||||
id: 'user-2',
|
||||
keycloakSub: 'f:realm:user2',
|
||||
email: 'user2@example.com',
|
||||
name: 'User Two',
|
||||
username: 'user2',
|
||||
picture: null,
|
||||
roles: [],
|
||||
lastLoginAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockFriendRequest = {
|
||||
id: 'request-1',
|
||||
senderId: 'user-1',
|
||||
receiverId: 'user-2',
|
||||
status: FriendRequestStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
sender: mockUser1,
|
||||
receiver: mockUser2,
|
||||
};
|
||||
|
||||
const mockFriendship = {
|
||||
id: 'friendship-1',
|
||||
userId: 'user-1',
|
||||
friendId: 'user-2',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const mockPrismaService = {
|
||||
user: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
friendRequest: {
|
||||
create: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateMany: jest.fn(),
|
||||
},
|
||||
friendship: {
|
||||
create: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
deleteMany: jest.fn(),
|
||||
},
|
||||
$transaction: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FriendsService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FriendsService>(FriendsService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('sendFriendRequest', () => {
|
||||
it('should send a friend request successfully', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser2);
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.friendRequest.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.friendRequest.create.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
|
||||
const result = await service.sendFriendRequest('user-1', 'user-2');
|
||||
|
||||
expect(result).toEqual(mockFriendRequest);
|
||||
expect(mockPrismaService.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'user-2' },
|
||||
});
|
||||
expect(mockPrismaService.friendRequest.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
senderId: 'user-1',
|
||||
receiverId: 'user-2',
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to send request to self', async () => {
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-1'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-1'),
|
||||
).rejects.toThrow('Cannot send friend request to yourself');
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when receiver does not exist', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'nonexistent'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'nonexistent'),
|
||||
).rejects.toThrow('User not found');
|
||||
});
|
||||
|
||||
it('should throw ConflictException when users are already friends', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser2);
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(mockFriendship);
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
).rejects.toThrow('You are already friends with this user');
|
||||
});
|
||||
|
||||
it('should throw ConflictException when request already exists', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser2);
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.friendRequest.findFirst.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
).rejects.toThrow('You already sent a friend request to this user');
|
||||
});
|
||||
|
||||
it('should throw ConflictException when reverse request exists', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser1);
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.friendRequest.findFirst.mockResolvedValue({
|
||||
...mockFriendRequest,
|
||||
senderId: 'user-2',
|
||||
receiverId: 'user-1',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
).rejects.toThrow('This user already sent you a friend request');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPendingReceivedRequests', () => {
|
||||
it('should return pending received requests', async () => {
|
||||
const requests = [mockFriendRequest];
|
||||
mockPrismaService.friendRequest.findMany.mockResolvedValue(requests);
|
||||
|
||||
const result = await service.getPendingReceivedRequests('user-2');
|
||||
|
||||
expect(result).toEqual(requests);
|
||||
expect(mockPrismaService.friendRequest.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
receiverId: 'user-2',
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPendingSentRequests', () => {
|
||||
it('should return pending sent requests', async () => {
|
||||
const requests = [mockFriendRequest];
|
||||
mockPrismaService.friendRequest.findMany.mockResolvedValue(requests);
|
||||
|
||||
const result = await service.getPendingSentRequests('user-1');
|
||||
|
||||
expect(result).toEqual(requests);
|
||||
expect(mockPrismaService.friendRequest.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
senderId: 'user-1',
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('acceptFriendRequest', () => {
|
||||
it('should accept a friend request and create friendship', async () => {
|
||||
const acceptedRequest = {
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.ACCEPTED,
|
||||
};
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
mockPrismaService.$transaction.mockResolvedValue([acceptedRequest]);
|
||||
|
||||
const result = await service.acceptFriendRequest('request-1', 'user-2');
|
||||
|
||||
expect(result).toEqual(acceptedRequest);
|
||||
expect(mockPrismaService.$transaction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when request does not exist', async () => {
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.acceptFriendRequest('nonexistent', 'user-2'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
await expect(
|
||||
service.acceptFriendRequest('nonexistent', 'user-2'),
|
||||
).rejects.toThrow('Friend request not found');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when user is not the receiver', async () => {
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.acceptFriendRequest('request-1', 'user-3'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
await expect(
|
||||
service.acceptFriendRequest('request-1', 'user-3'),
|
||||
).rejects.toThrow('You can only accept friend requests sent to you');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when request is already accepted', async () => {
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue({
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.ACCEPTED,
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.acceptFriendRequest('request-1', 'user-2'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
await expect(
|
||||
service.acceptFriendRequest('request-1', 'user-2'),
|
||||
).rejects.toThrow('Friend request is already accepted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('denyFriendRequest', () => {
|
||||
it('should deny a friend request', async () => {
|
||||
const deniedRequest = {
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.DENIED,
|
||||
};
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
mockPrismaService.friendRequest.update.mockResolvedValue(deniedRequest);
|
||||
|
||||
const result = await service.denyFriendRequest('request-1', 'user-2');
|
||||
|
||||
expect(result).toEqual(deniedRequest);
|
||||
expect(mockPrismaService.friendRequest.update).toHaveBeenCalledWith({
|
||||
where: { id: 'request-1' },
|
||||
data: { status: FriendRequestStatus.DENIED },
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when request does not exist', async () => {
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.denyFriendRequest('nonexistent', 'user-2'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when user is not the receiver', async () => {
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.denyFriendRequest('request-1', 'user-3'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when request is already denied', async () => {
|
||||
mockPrismaService.friendRequest.findUnique.mockResolvedValue({
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.DENIED,
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.denyFriendRequest('request-1', 'user-2'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFriends', () => {
|
||||
it('should return list of friends', async () => {
|
||||
const friendships = [
|
||||
{
|
||||
...mockFriendship,
|
||||
friend: mockUser2,
|
||||
},
|
||||
];
|
||||
mockPrismaService.friendship.findMany.mockResolvedValue(friendships);
|
||||
|
||||
const result = await service.getFriends('user-1');
|
||||
|
||||
expect(result).toEqual(friendships);
|
||||
expect(mockPrismaService.friendship.findMany).toHaveBeenCalledWith({
|
||||
where: { userId: 'user-1' },
|
||||
include: {
|
||||
friend: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unfriend', () => {
|
||||
it('should unfriend a user successfully', async () => {
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(mockFriendship);
|
||||
mockPrismaService.friendship.deleteMany.mockResolvedValue({ count: 2 });
|
||||
|
||||
await service.unfriend('user-1', 'user-2');
|
||||
|
||||
expect(mockPrismaService.friendship.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: 'user-1', friendId: 'user-2' },
|
||||
{ userId: 'user-2', friendId: 'user-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to unfriend self', async () => {
|
||||
await expect(service.unfriend('user-1', 'user-1')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
await expect(service.unfriend('user-1', 'user-1')).rejects.toThrow(
|
||||
'Cannot unfriend yourself',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when not friends', async () => {
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.unfriend('user-1', 'user-2')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
await expect(service.unfriend('user-1', 'user-2')).rejects.toThrow(
|
||||
'You are not friends with this user',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('areFriends', () => {
|
||||
it('should return true when users are friends', async () => {
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(mockFriendship);
|
||||
|
||||
const result = await service.areFriends('user-1', 'user-2');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when users are not friends', async () => {
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(null);
|
||||
|
||||
const result = await service.areFriends('user-1', 'user-2');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
273
src/friends/friends.service.ts
Normal file
273
src/friends/friends.service.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Logger,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../database/prisma.service';
|
||||
import { User, FriendRequest, FriendRequestStatus } from '@prisma/client';
|
||||
|
||||
export type FriendRequestWithRelations = FriendRequest & {
|
||||
sender: User;
|
||||
receiver: User;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FriendsService {
|
||||
private readonly logger = new Logger(FriendsService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async sendFriendRequest(
|
||||
senderId: string,
|
||||
receiverId: string,
|
||||
): Promise<FriendRequestWithRelations> {
|
||||
if (senderId === receiverId) {
|
||||
throw new BadRequestException('Cannot send friend request to yourself');
|
||||
}
|
||||
|
||||
const receiver = await this.prisma.user.findUnique({
|
||||
where: { id: receiverId },
|
||||
});
|
||||
|
||||
if (!receiver) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const existingFriendship = await this.areFriends(senderId, receiverId);
|
||||
if (existingFriendship) {
|
||||
throw new ConflictException('You are already friends with this user');
|
||||
}
|
||||
|
||||
const existingRequest = await this.prisma.friendRequest.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ senderId, receiverId, status: FriendRequestStatus.PENDING },
|
||||
{
|
||||
senderId: receiverId,
|
||||
receiverId: senderId,
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRequest) {
|
||||
if (existingRequest.senderId === senderId) {
|
||||
throw new ConflictException(
|
||||
'You already sent a friend request to this user',
|
||||
);
|
||||
} else {
|
||||
throw new ConflictException(
|
||||
'This user already sent you a friend request',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const friendRequest = await this.prisma.friendRequest.create({
|
||||
data: {
|
||||
senderId,
|
||||
receiverId,
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Friend request sent from ${senderId} to ${receiverId} (ID: ${friendRequest.id})`,
|
||||
);
|
||||
|
||||
return friendRequest;
|
||||
}
|
||||
|
||||
async getPendingReceivedRequests(
|
||||
userId: string,
|
||||
): Promise<FriendRequestWithRelations[]> {
|
||||
return this.prisma.friendRequest.findMany({
|
||||
where: {
|
||||
receiverId: userId,
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getPendingSentRequests(
|
||||
userId: string,
|
||||
): Promise<FriendRequestWithRelations[]> {
|
||||
return this.prisma.friendRequest.findMany({
|
||||
where: {
|
||||
senderId: userId,
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async acceptFriendRequest(
|
||||
requestId: string,
|
||||
userId: string,
|
||||
): Promise<FriendRequestWithRelations> {
|
||||
const friendRequest = await this.prisma.friendRequest.findUnique({
|
||||
where: { id: requestId },
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!friendRequest) {
|
||||
throw new NotFoundException('Friend request not found');
|
||||
}
|
||||
|
||||
if (friendRequest.receiverId !== userId) {
|
||||
throw new BadRequestException(
|
||||
'You can only accept friend requests sent to you',
|
||||
);
|
||||
}
|
||||
|
||||
if (friendRequest.status !== FriendRequestStatus.PENDING) {
|
||||
throw new BadRequestException(
|
||||
`Friend request is already ${friendRequest.status.toLowerCase()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const [updatedRequest] = await this.prisma.$transaction([
|
||||
this.prisma.friendRequest.update({
|
||||
where: { id: requestId },
|
||||
data: { status: FriendRequestStatus.ACCEPTED },
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.friendship.create({
|
||||
data: {
|
||||
userId: friendRequest.senderId,
|
||||
friendId: friendRequest.receiverId,
|
||||
},
|
||||
}),
|
||||
this.prisma.friendship.create({
|
||||
data: {
|
||||
userId: friendRequest.receiverId,
|
||||
friendId: friendRequest.senderId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
this.logger.log(
|
||||
`Friend request ${requestId} accepted. Users ${friendRequest.senderId} and ${friendRequest.receiverId} are now friends`,
|
||||
);
|
||||
|
||||
return updatedRequest;
|
||||
}
|
||||
|
||||
async denyFriendRequest(
|
||||
requestId: string,
|
||||
userId: string,
|
||||
): Promise<FriendRequestWithRelations> {
|
||||
const friendRequest = await this.prisma.friendRequest.findUnique({
|
||||
where: { id: requestId },
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!friendRequest) {
|
||||
throw new NotFoundException('Friend request not found');
|
||||
}
|
||||
|
||||
if (friendRequest.receiverId !== userId) {
|
||||
throw new BadRequestException(
|
||||
'You can only deny friend requests sent to you',
|
||||
);
|
||||
}
|
||||
|
||||
if (friendRequest.status !== FriendRequestStatus.PENDING) {
|
||||
throw new BadRequestException(
|
||||
`Friend request is already ${friendRequest.status.toLowerCase()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedRequest = await this.prisma.friendRequest.update({
|
||||
where: { id: requestId },
|
||||
data: { status: FriendRequestStatus.DENIED },
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Friend request ${requestId} denied by user ${userId}`);
|
||||
|
||||
return updatedRequest;
|
||||
}
|
||||
|
||||
async getFriends(userId: string) {
|
||||
return this.prisma.friendship.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
friend: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async unfriend(userId: string, friendId: string): Promise<void> {
|
||||
if (userId === friendId) {
|
||||
throw new BadRequestException('Cannot unfriend yourself');
|
||||
}
|
||||
|
||||
const friendship = await this.prisma.friendship.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
friendId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!friendship) {
|
||||
throw new NotFoundException('You are not friends with this user');
|
||||
}
|
||||
|
||||
await this.prisma.friendship.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId, friendId },
|
||||
{ userId: friendId, friendId: userId },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`User ${userId} unfriended user ${friendId}`);
|
||||
}
|
||||
|
||||
async areFriends(userId: string, friendId: string): Promise<boolean> {
|
||||
const friendship = await this.prisma.friendship.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
friendId,
|
||||
},
|
||||
});
|
||||
|
||||
return !!friendship;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user