Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add chat actions #104

Merged
merged 11 commits into from
Nov 21, 2023
52 changes: 52 additions & 0 deletions backend/src/chat/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Get,
HttpException,
HttpStatus,
Logger,
ParseIntPipe,
Post,
Query,
Expand All @@ -17,6 +18,28 @@ import { chatMemberRole, chatType } from '@prisma/client';
@Controller('chat*')
export class ChatController {
constructor(private chatService: ChatService) {}
private readonly logger = new Logger('chat.controller');

async notValidAction(
chatId: number,
login: string,
user: string,
): Promise<boolean> {
const you = await this.chatService.getMemberFromChat(chatId, login);
const member = await this.chatService.getMemberFromChat(chatId, user);
// Me and him must exist in the database
if (!you || !member) {
this.logger.error('Unable to find user or member');
return true;
}
// I cannot be the member I want to mute, I cannot be member to mute, I cannot mute an admin nor the owner
if (you === member || you.role === 'MEMBER' || member.role !== 'MEMBER') {
this.logger.error('You are not allowed to mute this user');
return true;
}

return false;
}

@Post('/create')
async createChat(
Expand Down Expand Up @@ -97,6 +120,35 @@ export class ChatController {
return messages;
}

@Post('/mute')
async muteMember(
@Body('chatId') chatId: number,
@Body('login') login: string,
@Body('user') user: string,
) {
if (await this.notValidAction(chatId, login, user)) {
throw new HttpException(
{
status: HttpStatus.FORBIDDEN,
error: 'You are not allowed to mute this user',
},
HttpStatus.FORBIDDEN,
);
}
const updatedChat = await this.chatService.muteUserFromChat(chatId, user);

if (!updatedChat) {
throw new HttpException(
{
status: HttpStatus.NOT_MODIFIED,
error: 'Unable to handle this action',
},
HttpStatus.NOT_MODIFIED,
);
}
return updatedChat;
}

@Post('/ban')
async banMember(
@Body('chatId') chatId: number,
Expand Down
106 changes: 78 additions & 28 deletions backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,34 +76,6 @@ export class ChatGateway
await this.usersService.updateUserStatus(id, 'OFFLINE');
}

// TODO:
async handleConnection(@ConnectedSocket() client: Socket) {
const { login, id } = client.handshake.auth?.user;

// TODO: remove this hardcoded user id
if (!login) {
client.emit('connected', { error: 'User not found' });
client.disconnect();
return;
}

// change user status to online
await this.usersService.updateUserStatus(id, 'ONLINE');

this.connectedUsers[login] = client;
client.emit('connected', { message: `You are connected as ${login}` });

// const allChats = await this.chatService.listChats();
const chats = await this.chatService.listChatsByUserLogin(login);

// TODO: Remove this after implementing chat rooms
for (const chat of chats) {
client.join(`chat:${chat.id.toString()}`);
}

this.logger.log(`User ${login} connected`);
}

private validateConnection(client: Socket) {
const token = client.handshake.auth.token;

Expand Down Expand Up @@ -400,6 +372,55 @@ export class ChatGateway
}
}

@SubscribeMessage('promoteToAdmin')
async promoteToAdmin(
@MessageBody('user') user: string,
@MessageBody('chatId', new ParseIntPipe()) chatId: number,
@ConnectedSocket() client: Socket,
) {
const login = client.handshake.auth?.user?.login;
if (
await this.notValidAction('promoteToAdmin', chatId, login, user, client)
) {
return;
}
const updatedChat = await this.chatService.giveAdmin(chatId, [user]);
// demote the current user to admin
if (!updatedChat) {
this.logger.error('Failed to promote user');
return;
}
this.logger.log(`You promoted ${user} to admin of chat ${chatId}`);
}

@SubscribeMessage('demoteToMember')
async demoteToMember(
@MessageBody('user') user: string,
@MessageBody('chatId', new ParseIntPipe()) chatId: number,
) {
const member = await this.chatService.getMemberFromChat(chatId, user);
const you = await this.chatService.getMemberFromChat(chatId, user);
if (!member || !you) {
this.logger.error('User not found');
return;
}
if (you.role !== 'OWNER') {
this.logger.error('Only the chat owner can demote admins');
return;
}
if (member.role !== 'ADMIN') {
this.logger.error('You cannot demote non-admin users');
return;
}
const updatedChat = await this.chatService.giveMember(chatId, [user]);
// demote the current user to admin
if (!updatedChat) {
this.logger.error('Failed to demote user');
return;
}
this.logger.log(`You demoted ${user} to member of chat ${chatId}`);
}

// TODO: Add rule, if you are banned you cannot invite people to this chat
// TODO: Drop this rule and replace it by an invite event
@SubscribeMessage('addToChat')
Expand Down Expand Up @@ -494,10 +515,12 @@ export class ChatGateway
const member = await this.chatService.getMemberFromChat(chatId, user);

if (!you || !member) {
this.logger.error('User not found');
client.emit(event, { error: 'User not found' });
return true;
}
if (you === member || you.role === 'MEMBER' || member.role !== 'MEMBER') {
this.logger.error('You are not allowed to do this action');
client.emit(event, { error: 'You are not allowed to do this action' });
return true;
}
Expand Down Expand Up @@ -599,4 +622,31 @@ export class ChatGateway
);
return numberOfUsers;
}

async handleConnection(@ConnectedSocket() client: Socket) {
const { login, id } = client.handshake.auth?.user;

// TODO: remove this hardcoded user id
if (!login) {
client.emit('connected', { error: 'User not found' });
client.disconnect();
return;
}

// change user status to online
await this.usersService.updateUserStatus(id, 'ONLINE');

this.connectedUsers[login] = client;
client.emit('connected', { message: `You are connected as ${login}` });

// const allChats = await this.chatService.listChats();
const chats = await this.chatService.listChatsByUserLogin(login);

// TODO: Remove this after implementing chat rooms
for (const chat of chats) {
client.join(`chat:${chat.id.toString()}`);
}

this.logger.log(`User ${login} connected`);
}
}
50 changes: 50 additions & 0 deletions backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,56 @@ export class ChatService {
}
}

async giveMember(chatId: number, guestList: string[]): Promise<Chat> {
try {
// First make sure that every member in the guest list is actually a member of the chat
const existingUsers = await this.prisma.chatMember.findMany({
where: {
chatId,
userLogin: {
in: guestList,
},
},
});
if (
existingUsers.some(
(existingUser) => existingUser.role === chatMemberRole.OWNER,
)
) {
console.log(`Some users in the guest list are chat owners`);
return null;
}
if (existingUsers.length !== guestList.length) {
console.log(
`Some users in the guest list are not members of chat ${chatId}`,
);
return null;
}
const updatedChat = await this.prisma.chat.update({
where: {
id: chatId,
},
data: {
users: {
updateMany: guestList.map((guest) => ({
where: {
userLogin: guest,
},
data: {
role: chatMemberRole.MEMBER,
},
})),
},
},
});

return updatedChat;
} catch (error) {
console.log(error);
return null;
}
}

async banUserFromChat(chatId: number, member: string): Promise<Chat> {
try {
const updatedChat = await this.prisma.chat.update({
Expand Down
53 changes: 50 additions & 3 deletions backend/src/database/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ model MatchHistory {
id Int @id @default(autoincrement())

winner User @relation(fields: [winnerId], references: [id], name: "winner")
winnerId String
winnerId String
winnerPoints Int

loser User @relation(fields: [loserId], references: [id], name: "loser")
loserId String
loserId String
loserPoints Int

createdAt DateTime @default(now())
Expand All @@ -73,17 +73,42 @@ async function main() {
login: faker.internet.userName(),
displayName: faker.person.firstName(),
email: faker.internet.email(),
avatar: faker.image.avatar(),
avatar: '',
victory: Math.floor(Math.random() * 100),
mfaEnabled: false,
mfaSecret: 'secret',
}));
// TODO: change to your user when testing
// users[0].login = 'vwildner';
// users[0].displayName = 'Victor Wildner';

await prisma.user.createMany({
data: users,
});

const createdUsers = await prisma.user.findMany({
select: {
id: true,
login: true,
},
});

const getRandomUserLogin = () => {
return createdUsers[Math.floor(Math.random() * 49) + 1].login;
};

// create 3 chats with 3 members each
const chats: any = Array.from({ length: 3 }).map(() => ({
name: faker.hacker.adjective(),
chatType: 'PUBLIC',
owner: createdUsers[0].login,
}));

await prisma.chat.createMany({
data: chats,
});

const createdChats = await prisma.chat.findMany({
select: {
id: true,
},
Expand All @@ -94,6 +119,28 @@ async function main() {
return createdUsers[Math.floor(Math.random() * 50)].id;
};

// Add 3 members to each chat
const chatMembers: any = Array.from({ length: 9 }).map(() => ({
chatId: createdChats[Math.floor(Math.random() * 3)].id,
userLogin: getRandomUserLogin(),
role: 'MEMBER',
}));

await prisma.chatMember.createMany({
data: chatMembers,
});

// Add the chat owner to each chat
const chatOwners: any = Array.from({ length: 3 }).map((_, index) => ({
chatId: createdChats[index].id,
userLogin: createdUsers[0].login,
role: 'OWNER',
}));

await prisma.chatMember.createMany({
data: chatOwners,
});

const matchHistory = Array.from({ length: 50 }).map(() => ({
winnerId: getRandomUserId(),
winnerPoints: Math.floor(Math.random() * 5) + 5,
Expand Down
2 changes: 0 additions & 2 deletions backend/src/friends/friends.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import {
Delete,
Get,
Post,
Put,
Req,
UseGuards,
} from '@nestjs/common';
import { FriendsService } from './friends.service';
import { AccessTokenGuard } from 'src/auth/jwt/jwt.guard';
import { User } from '@prisma/client';
import { create } from 'domain';
import { CreateFriendDto } from './dto/createFriendDto';
import { DeleteFriendDto } from './dto/deleteFriendDto';

Expand Down
4 changes: 2 additions & 2 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ async function bootstrap() {
);

app.enableCors({
origin:[
origin: [
process.env.FRONTEND_URL,
'http://localhost:3000',
'http://42transcendence.me',
'http://www.42transcendence.me',
'http://api.42transcendence.me',
'https://42transcendence.me',
'https://www.42transcendence.me',
'https://api.42transcendence.me'
'https://api.42transcendence.me',
],
credentials: true,
});
Expand Down
6 changes: 5 additions & 1 deletion backend/src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import {
Injectable,
NotFoundException,
BadRequestException,
} from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateUserDto } from './dto/createUser.dto';
import { UpdateUserDto } from './dto/updateUser.dto';
Expand Down
Loading
Loading