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

[backend] mute user in room #216

Merged
merged 19 commits into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2fa8f63
[backend] Add test for PUT /room/:id/mutes/:userId
lim396 Jan 18, 2024
0650dfe
[backend] Add test for GET /room/:id/mutes
lim396 Jan 18, 2024
c228c46
[backend] Add test to ensure that messages are not received from user…
lim396 Jan 18, 2024
3f7bcfc
[backend] Add MuteUserOnRoom model and migrate
lim396 Jan 18, 2024
e1505e5
[backend] Add API PUT /room/:roomId/mutes/:userId and GET /room/:room…
lim396 Jan 18, 2024
6260f2f
[backend] Modified to not save and send messages from users in mute
lim396 Jan 18, 2024
64e12aa
[backend] Add test for DELETE /room/:roomId/mutes/:userId
lim396 Jan 18, 2024
d1e9397
[backend] Add test to confirm messages from unmuted users can be rece…
lim396 Jan 18, 2024
5fbb6ef
[backend] Add API DELETE /room/:roomId/mutes/:userId
lim396 Jan 18, 2024
e8d0959
[backend] Separate test items and add status code to description
lim396 Jan 19, 2024
adbe1c4
[backend] e2e test on chat-gateway made a little easier to see
lim396 Jan 19, 2024
072ff1c
[backend] Removed tests implemented in a separate file from room e2e …
lim396 Jan 19, 2024
be06885
[backend] Fix test description
lim396 Jan 19, 2024
13deab7
[backend] Add test to confirm that it is possible to remute after a m…
lim396 Jan 20, 2024
6c23d21
[backend] Changed to be able to remute without unmuting after mute pe…
lim396 Jan 20, 2024
d21c5ce
[backend] Add test to confirm that it can be muted indefinitely
lim396 Jan 20, 2024
b528900
[backend] Fixed to be able to mute indefinitely
lim396 Jan 20, 2024
8dd04ee
[backend] Fix test
lim396 Jan 20, 2024
be7f8b3
[backend] Refactored code for indefinite mute when no duration is spe…
lim396 Jan 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "MuteUserOnRoom" (
"userId" INTEGER NOT NULL,
"roomId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3)
);

-- CreateIndex
CREATE UNIQUE INDEX "MuteUserOnRoom_userId_roomId_key" ON "MuteUserOnRoom"("userId", "roomId");

-- AddForeignKey
ALTER TABLE "MuteUserOnRoom" ADD CONSTRAINT "MuteUserOnRoom_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "MuteUserOnRoom" ADD CONSTRAINT "MuteUserOnRoom_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "Room"("id") ON DELETE CASCADE ON UPDATE CASCADE;
15 changes: 15 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ model User {
twoFactorEnabled Boolean @default(false)
Message Message[]
BannedRooms BanUserOnRoom[]
MutedRooms MuteUserOnRoom[]
}

model Room {
Expand All @@ -52,6 +53,7 @@ model Room {
accessLevel AccessLevel
password String?
BannedUsers BanUserOnRoom[]
MutedUsers MuteUserOnRoom[]
}

enum AccessLevel {
Expand Down Expand Up @@ -91,6 +93,19 @@ model BanUserOnRoom {
@@unique(fields: [userId, roomId], name: "userId_roomId_unique")
}

model MuteUserOnRoom {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int

room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
roomId Int

createdAt DateTime @default(now())
expiresAt DateTime?

@@unique(fields: [userId, roomId], name: "userId_roomId_unique")
}

model Message {
id Int @id @default(autoincrement())
content String
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FriendRequestModule } from './friend-request/friend-request.module';
import { HistoryModule } from './history/history.module';
import { PrismaModule } from './prisma/prisma.module';
import { BanModule } from './room/ban/ban.module';
import { MuteModule } from './room/mute/mute.module';
import { RoomModule } from './room/room.module';
import { UserModule } from './user/user.module';

Expand All @@ -19,6 +20,7 @@ import { UserModule } from './user/user.module';
PrismaModule,
AuthModule,
BanModule,
MuteModule,
RoomModule,
EventsModule,
ChatModule,
Expand Down
11 changes: 10 additions & 1 deletion backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { Server, Socket } from 'socket.io';
import { RoomLeftEvent } from 'src/common/events/room-left.event';
import { ChatService } from './chat.service';
import { MuteService } from 'src/room/mute/mute.service';
import { CreateMessageDto } from './dto/create-message.dto';
import { MessageEntity } from './entities/message.entity';

Expand All @@ -21,7 +22,10 @@ import { MessageEntity } from './entities/message.entity';
cookie: true,
})
export class ChatGateway {
constructor(private readonly chatService: ChatService) {}
constructor(
private readonly chatService: ChatService,
private readonly muteService: MuteService,
) {}

@WebSocketServer()
server: Server;
Expand All @@ -46,6 +50,11 @@ export class ChatGateway {
return;
}

const MutedUsers = await this.muteService.findAll(data.roomId);
if (MutedUsers.some((user) => user.id === data.userId)) {
return;
}

// Save message to the database
await this.chatService.createMessage(data);

Expand Down
3 changes: 2 additions & 1 deletion backend/src/chat/chat.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { AuthModule } from 'src/auth/auth.module';
import { MuteService } from 'src/room/mute/mute.service';
import { PrismaModule } from 'src/prisma/prisma.module';
import { UserService } from '../user/user.service';
import { ChatController } from './chat.controller';
Expand All @@ -8,7 +9,7 @@ import { ChatService } from './chat.service';

@Module({
controllers: [ChatController],
providers: [ChatGateway, ChatService, UserService],
providers: [ChatGateway, ChatService, MuteService, UserService],
imports: [PrismaModule, AuthModule],
})
export class ChatModule {}
9 changes: 9 additions & 0 deletions backend/src/room/mute/dto/create-mute.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional } from 'class-validator';

export class CreateMuteDto {
@IsNumber()
@IsOptional()
@ApiProperty({ required: false })
duration: number;
}
4 changes: 4 additions & 0 deletions backend/src/room/mute/dto/update-mute.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateMuteDto } from './create-mute.dto';

export class UpdateMuteDto extends PartialType(CreateMuteDto) {}
1 change: 1 addition & 0 deletions backend/src/room/mute/entities/mute.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class Mute {}
21 changes: 21 additions & 0 deletions backend/src/room/mute/mute.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/prisma/prisma.service';
import { MuteController } from './mute.controller';
import { MuteService } from './mute.service';

describe('MuteController', () => {
let controller: MuteController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MuteController],
providers: [MuteService, PrismaService],
}).compile();

controller = module.get<MuteController>(MuteController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
48 changes: 48 additions & 0 deletions backend/src/room/mute/mute.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Controller,
Get,
Put,
Body,
Delete,
Param,
ParseIntPipe,
UseGuards,
} from '@nestjs/common';
import { CreateMuteDto } from './dto/create-mute.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { AdminGuard } from '../guards/admin.guard';
import { MuteService } from './mute.service';

@ApiTags('mute')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('room/:roomId/mutes')
export class MuteController {
constructor(private readonly muteService: MuteService) {}

@Put(':userId')
@UseGuards(AdminGuard)
create(
@Param('roomId', ParseIntPipe) roomId: number,
@Param('userId', ParseIntPipe) userId: number,
@Body() createMuteDto: CreateMuteDto,
) {
return this.muteService.create(roomId, userId, createMuteDto);
}

@Get()
@UseGuards(AdminGuard)
findAll(@Param('roomId', ParseIntPipe) roomId: number) {
return this.muteService.findAll(roomId);
}

@Delete(':userId')
@UseGuards(AdminGuard)
remove(
@Param('roomId', ParseIntPipe) roomId: number,
@Param('userId', ParseIntPipe) userId: number,
) {
return this.muteService.remove(roomId, userId);
}
}
11 changes: 11 additions & 0 deletions backend/src/room/mute/mute.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/prisma/prisma.module';
import { MuteService } from './mute.service';
import { MuteController } from './mute.controller';

@Module({
controllers: [MuteController],
providers: [MuteService],
imports: [PrismaModule],
})
export class MuteModule {}
19 changes: 19 additions & 0 deletions backend/src/room/mute/mute.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/prisma/prisma.service';
import { MuteService } from './mute.service';

describe('MuteService', () => {
let service: MuteService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MuteService, PrismaService],
}).compile();

service = module.get<MuteService>(MuteService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
136 changes: 136 additions & 0 deletions backend/src/room/mute/mute.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateMuteDto } from './dto/create-mute.dto';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class MuteService {
constructor(private readonly prisma: PrismaService) {}

async isExpired(roomId: number, userId: number) {
const now = new Date();
const mute = await this.prisma.muteUserOnRoom.findUnique({
where: {
userId_roomId_unique: {
userId,
roomId,
},
},
});
if (!mute) {
return false;
} else if (mute.expiresAt === null) {
return false;
} else if (mute.expiresAt <= now) {
return true;
} else {
return false;
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こういう名前の関数で、これが書かれない気がする


async create(roomId: number, userId: number, createMuteDto: CreateMuteDto) {
await this.prisma.$transaction(async (prisma) => {
const room = await prisma.room.findUnique({
where: {
id: roomId,
},
});
if (room.accessLevel === 'DIRECT') {
throw new BadRequestException('Cannot mute user in DIRECT room');
}
const user = await prisma.userOnRoom.findUnique({
where: {
userId_roomId_unique: {
userId,
roomId,
},
},
});
if (!user) {
throw new NotFoundException('User does not exist in the room');
}
if (user.role === 'OWNER') {
throw new ForbiddenException('Cannot mute owner');
}
if (await this.isExpired(roomId, userId)) {
await this.remove(roomId, userId);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここまでまとめてremoveIfAlreadyExistsAndExpiredみたいな(もっといい名前考えてほしいですが)ふうに切り分けた方が自然かな

      if (await this.isExpired(roomId, userId)) {
        await this.remove(roomId, userId);
      }

let expiresAt;
if (!createMuteDto.duration) {
expiresAt = null;
} else {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + createMuteDto.duration);
}
await prisma.muteUserOnRoom.create({
data: {
userId,
roomId,
expiresAt,
},
});
});
return {
message: 'Mute user successfully',
};
}

async findAll(roomId: number) {
const now = new Date();
const tmp = await this.prisma.muteUserOnRoom.findMany({
where: {
roomId,
OR: [
{
expiresAt: {
gt: now,
},
},
{
expiresAt: null,
},
],
},
include: {
user: {
select: {
id: true,
name: true,
avatarURL: true,
},
},
},
});
const users = tmp.map((item) => item.user);
return users;
}

async remove(roomId: number, userId: number) {
await this.prisma.$transaction(async (prisma) => {
const user = await prisma.muteUserOnRoom.findUnique({
where: {
userId_roomId_unique: {
userId,
roomId,
},
},
});
if (!user) {
throw new NotFoundException('User not found in the Mute list');
}
await prisma.muteUserOnRoom.delete({
where: {
userId_roomId_unique: {
userId,
roomId,
},
},
});
return { message: 'Unmute user successfully' };
});
}
}
Loading