friends system

This commit is contained in:
2025-12-08 01:42:39 +08:00
parent c8cfca8de8
commit 15a1551eb6
16 changed files with 1905 additions and 15 deletions

View 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,
};
}
}