diff --git a/back/volume/src/chat/chat.controller.ts b/back/volume/src/chat/chat.controller.ts new file mode 100644 index 0000000..f43d86e --- /dev/null +++ b/back/volume/src/chat/chat.controller.ts @@ -0,0 +1,177 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + NotFoundException, + Param, + ParseIntPipe, + Post, + UseGuards +} from '@nestjs/common' +import { AuthenticatedGuard } from 'src/auth/42-auth.guard' +import { UsersService } from 'src/users/users.service' +import { ChannelService } from './chat.service' + +import { CreateChannelDto } from './dto/create-channel.dto' +import { IdDto, PasswordDto, MuteDto } from './dto/updateUser.dto' + +import type User from 'src/users/entity/user.entity' +import type Channel from './entity/channel.entity' +import { Profile42 } from 'src/auth/42.decorator' +import { Profile } from 'passport-42' + +@Controller('channels') +export class ChatController { + constructor ( + private readonly channelService: ChannelService, + private readonly usersService: UsersService + ) {} + + @Post(':id/invite') + @UseGuards(AuthenticatedGuard) + async addUser ( + @Param('id') id: number, @Body() target: IdDto, + @Profile42() profile: Profile) { + const channel = await this.channelService.getFullChannel(id) + const user: User | null = await this.usersService.findUser(target.id) + if (user == null) throw new NotFoundException(`User #${target.id} not found`) + if (!await this.channelService.isUser(channel.id, +profile.id)) + throw new BadRequestException('You are not allowed to invite users to this channel') + if (await this.channelService.isUser(channel.id, target.id)) + throw new BadRequestException('User is already in this channel') + if (await this.channelService.isBanned(channel.id, target.id)) + throw new BadRequestException('User is banned from this channel') + channel.users.push(user) + this.channelService.save(channel) + } + + @Delete(':id/kick') + @UseGuards(AuthenticatedGuard) + async removeUser ( + @Param('id') id: number, @Body() target: IdDto, + @Profile42() profile: Profile) { + const channel = await this.channelService.getFullChannel(id) + if (!await this.channelService.isAdmin(channel.id, +profile.id)) + throw new BadRequestException('You are not allowed to kick users from this channel') + if (!await this.channelService.isUser(channel.id, target.id)) + throw new BadRequestException('User is not in this channel') + if (await this.channelService.isOwner(channel.id, target.id)) + throw new BadRequestException('You cannot kick the owner of the channel') + channel.users = channel.users.filter((usr: User) => { + return usr.ftId !== target.id + }) + this.channelService.save(channel) + } + + @Post(':id/admin') + @UseGuards(AuthenticatedGuard) + async addAdmin ( + @Param('id') id: number, + @Body() target: IdDto, + @Profile42() profile: Profile) { + const channel = await this.channelService.getFullChannel(id) + const user: User | null = await this.usersService.findUser(target.id) + if (user == null) throw new NotFoundException(`User #${target.id} not found`) + if (!await this.channelService.isOwner(channel.id, +profile.id)) + throw new BadRequestException('You are not the owner of this channel') + if (!await this.channelService.isUser(channel.id, target.id)) + throw new BadRequestException('User is not in this channel') + if (await this.channelService.isAdmin(channel.id, target.id)) + throw new BadRequestException('User is already an admin of this channel') + channel.admins.push(user) + this.channelService.save(channel) + } + + @Delete(':id/admin') + @UseGuards(AuthenticatedGuard) + async removeAdmin ( + @Param('id') id: number, + @Body() target: IdDto, + @Profile42() profile: Profile) { + const channel = await this.channelService.getFullChannel(id) + if (!await this.channelService.isOwner(channel.id, +profile.id)) + throw new BadRequestException('You are not the owner of this channel') + if (!await this.channelService.isAdmin(channel.id, target.id)) + throw new BadRequestException('User is not an admin of this channel') + channel.admins = channel.admins.filter((usr: User) => { + return usr.ftId !== target.id + }) + this.channelService.save(channel) + } + + @Post(':id/ban') + @UseGuards(AuthenticatedGuard) + async addBan ( + @Param('id') id: number, + @Body() target: IdDto, + @Profile42() profile: Profile) { + const channel = await this.channelService.getFullChannel(id) + const user: User | null = await this.usersService.findUser(target.id) + if (user == null) throw new NotFoundException(`User #${target.id} not found`) + if (!await this.channelService.isAdmin(channel.id, +profile.id)) + throw new BadRequestException('You are not allowed to ban users from this channel') + if (await this.channelService.isOwner(channel.id, target.id)) + throw new BadRequestException('You cannot ban the owner of the channel') + if (await this.channelService.isBanned(channel.id, target.id)) + throw new BadRequestException('User is already banned from this channel') + channel.banned.push(user) + this.channelService.save(channel) + } + + @Post(':id/mute') + @UseGuards(AuthenticatedGuard) + async addMute ( + @Param('id') id: number, + @Body() mute: MuteDto, // [userId, duration] + @Profile42() profile: Profile) { + const channel = await this.channelService.getFullChannel(id) + const user: User | null = await this.usersService.findUser(mute.data[0]) + if (user == null) throw new NotFoundException(`User #${mute.data[0]} not found`) + if (!await this.channelService.isAdmin(channel.id, +profile.id)) + throw new BadRequestException('You are not allowed to mute users from this channel') + if (await this.channelService.isOwner(channel.id, mute.data[0])) + throw new BadRequestException('You cannot mute the owner of the channel') + if (await this.channelService.getMuteDuration(channel.id, mute.data[0]) > 0) + throw new BadRequestException('User is already muted from this channel') + channel.muted.push(mute.data) + this.channelService.save(channel) + } + + @Delete(':id') + @UseGuards(AuthenticatedGuard) + async deleteChannel ( + @Profile42() profile: Profile, + @Param('id') id: number + ) { + if (!await this.channelService.isOwner(id, +profile.id)) + throw new BadRequestException('You are not the owner of this channel') + await this.channelService.removeChannel(id) + return + } + + @Post(':id/password') + @UseGuards(AuthenticatedGuard) + async updatePassword ( + @Profile42() profile: Profile, + @Param('id') id: number, + @Body() data: PasswordDto + ) { + if (await this.channelService.isOwner(id, +profile.id)) + throw new BadRequestException('You are not the owner of this channel') + await this.channelService.updatePassword(id, data.password) + } + + + @Get() + @UseGuards(AuthenticatedGuard) + async getChannelsForUser (@Profile42() profile: Profile): Promise { + return await this.channelService.getChannelsForUser(+profile.id) + } + + @Post() + async createChannel (@Body() channel: CreateChannelDto) { + return await this.channelService.createChannel(channel) + } +} diff --git a/back/volume/src/chat/chat.gateway.ts b/back/volume/src/chat/chat.gateway.ts index 0d47de3..3e54192 100644 --- a/back/volume/src/chat/chat.gateway.ts +++ b/back/volume/src/chat/chat.gateway.ts @@ -1,3 +1,4 @@ +/* import { UnauthorizedException, UseGuards } from '@nestjs/common' import { type OnGatewayConnection, @@ -99,3 +100,4 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { /// TODO: Send message to users } } +*/ diff --git a/back/volume/src/chat/chat.module.ts b/back/volume/src/chat/chat.module.ts index 930ebb4..8fcaa1f 100644 --- a/back/volume/src/chat/chat.module.ts +++ b/back/volume/src/chat/chat.module.ts @@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm' import { AuthModule } from 'src/auth/auth.module' import { UsersModule } from 'src/users/users.module' -import { ChatGateway } from './chat.gateway' -import { ChatService } from './chat.service' -import { Channel } from './entity/channel.entity' -import { Message } from './entity/message.entity' +// import { ChatGateway } from './chat.gateway' +import { ChatController } from './chat.controller' +import { ChannelService } from './chat.service' + +import Channel from './entity/channel.entity' +import Message from './entity/message.entity' @Module({ imports: [ @@ -15,6 +17,7 @@ import { Message } from './entity/message.entity' TypeOrmModule.forFeature([Channel]), TypeOrmModule.forFeature([Message]) ], - providers: [ChatGateway, ChatService] + controllers: [ChatController], + providers: [ChannelService] }) export class ChatModule {} diff --git a/back/volume/src/chat/chat.service.ts b/back/volume/src/chat/chat.service.ts index 525949d..1ee459d 100644 --- a/back/volume/src/chat/chat.service.ts +++ b/back/volume/src/chat/chat.service.ts @@ -1,56 +1,139 @@ -import { Injectable } from '@nestjs/common' +import { Injectable, NotFoundException } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' -import { type User } from 'src/users/entity/user.entity' -import { Channel } from './entity/channel.entity' -import { Message } from './entity/message.entity' + +import { type CreateChannelDto } from './dto/create-channel.dto' +import { UsersService } from 'src/users/users.service' + +import type User from 'src/users/entity/user.entity' +import Channel from './entity/channel.entity' @Injectable() -export class ChatService { +export class ChannelService { constructor ( @InjectRepository(Channel) private readonly ChannelRepository: Repository, - @InjectRepository(Message) - private readonly MessageRepository: Repository + private readonly usersService: UsersService ) {} - async createChannel (Channel: Channel, creator: User): Promise { - const newChannel = await this.addCreatorToChannel(Channel, creator) + async createChannel (channel: CreateChannelDto): Promise { + const user: User | null = await this.usersService.findUser(channel.owner) + if (user == null) + throw new NotFoundException(`User #${channel.owner} not found`) + const newChannel = new Channel() + newChannel.owner = user + newChannel.users = [user] + newChannel.admins = [user] + newChannel.name = channel.name + newChannel.isPrivate = channel.isPrivate + newChannel.password = channel.password return await this.ChannelRepository.save(newChannel) } - async getChannelsForUser (userId: number): Promise { - return await this.ChannelRepository.find({}) // where userId is in User[] of channel? + async updatePassword (id: number, password: string) { + let channel: Channel | null = await this.ChannelRepository.findOneBy({id}) + if (channel === null) { throw new NotFoundException(`Channel #${id} not found`) } + channel.password = password + await this.ChannelRepository.save(channel) + } + + async getChannelsForUser (ftId: number): Promise { + let rooms: Channel[] = [] + rooms = [ + ...(await this.ChannelRepository.createQueryBuilder('room') + .where('room.isPrivate = false') + .getMany()) + ] + + rooms = [ + ...rooms, + ...(await this.ChannelRepository.createQueryBuilder('room') + .innerJoin('room.users', 'users') + .where('room.isPrivate = true') + .andWhere('users.ftId = :ftId', { ftId }) + .getMany()) + ] + return rooms + } + + async addUserToChannel (channel: Channel, user: User): Promise { + channel.owner = user + return await this.ChannelRepository.save(channel) } - async addCreatorToChannel (Channel: Channel, creator: User): Promise { - Channel.users.push(creator) - return Channel + async getChannel (id: number): Promise { + const channel = await this.ChannelRepository.findOneBy({ id }) + if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) } + return channel } - async createMessage (message: Message): Promise { - return await this.MessageRepository.save( - this.MessageRepository.create(message) - ) + async getFullChannel (id: number): Promise { + const channel = await this.ChannelRepository.findOne({ + where: { id }, + relations: ['users', 'admins', 'banned', 'muted', 'owner'] + }) + if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) } + return channel } - async deleteBySocketId (socketId: string) { - return await this.ChannelRepository.delete({}) // for disconnect + async update (channel: Channel) { + await this.ChannelRepository.update(channel.id, channel) } - async getChannel (id: number): Promise { - return await this.ChannelRepository.findOneBy({ id }) + async save (channel: Channel) { + await this.ChannelRepository.save(channel) } - async findMessagesInChannelForUser ( - channel: Channel, - user: User - ): Promise { - return await this.MessageRepository.createQueryBuilder('message') - .where('message.channel = :chan', { chan: channel }) - .andWhere('message.author NOT IN (:...blocked)', { - blocked: user.blocked - }) - .getMany() + async removeChannel (channelId: number) { + await this.ChannelRepository.delete(channelId) + } + + async isOwner (id: number, userId: number): Promise { + const channel = await this.ChannelRepository.findOne({ + where: { id }, + relations: { owner: true } + }) + if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) } + return channel.owner.ftId === userId + } + + async isAdmin (id: number, userId: number): Promise { + const channel = await this.ChannelRepository.findOne({ + where: { id }, + relations: { admins: true } + }) + if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) } + return channel.admins.findIndex((user) => user.ftId === userId) != -1 + } + + async isUser (id: number, userId: number): Promise { + const channel = await this.ChannelRepository.findOne({ + where: { id }, + relations: { users: true } + }) + if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) } + return channel.users.findIndex((user) => user.ftId === userId) != -1 + } + + async isBanned (id: number, userId: number): Promise { + const channel = await this.ChannelRepository.findOne({ + where: { id }, + relations: { banned: true } + }) + if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) } + return channel.banned.findIndex((user) => user.ftId === userId) != -1 + } + + async getMuteDuration (id: number, userId: number): Promise { + const channel = await this.ChannelRepository.findOne({ + where: { id }, + relations: { muted: true } + }) + if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) } + + const mutation: Array | undefined = channel.muted.find((mutation) => mutation[0] === userId) + if (mutation == null) { return 0 } + return mutation[1] + } } diff --git a/back/volume/src/chat/dto/connection.dto.ts b/back/volume/src/chat/dto/connection.dto.ts new file mode 100644 index 0000000..c70f38c --- /dev/null +++ b/back/volume/src/chat/dto/connection.dto.ts @@ -0,0 +1,13 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator' + +export class ConnectionDto { + @IsNumber() + UserId: number + + @IsNumber() + ChannelId: number + + @IsString() + @IsOptional() + pwd: string +} diff --git a/back/volume/src/chat/dto/create-channel.dto.ts b/back/volume/src/chat/dto/create-channel.dto.ts new file mode 100644 index 0000000..af1f03a --- /dev/null +++ b/back/volume/src/chat/dto/create-channel.dto.ts @@ -0,0 +1,28 @@ +import { Transform } from 'class-transformer' +import { + IsPositive, + IsAlpha, + IsString, + IsOptional, + IsNumber, + IsBoolean +} from 'class-validator' + +export class CreateChannelDto { + @IsOptional() + @IsPositive() + id: number + + @IsString() + name: string + + @IsNumber() + owner: number + + @IsOptional() + password: string + + @IsBoolean() + @Transform(({ value }) => value === 'true') + isPrivate: boolean +} diff --git a/back/volume/src/chat/dto/create-message.dto.ts b/back/volume/src/chat/dto/create-message.dto.ts new file mode 100644 index 0000000..56419a0 --- /dev/null +++ b/back/volume/src/chat/dto/create-message.dto.ts @@ -0,0 +1,12 @@ +import { IsNumber, IsString } from 'class-validator' + +export class CreateMessageDto { + @IsString() + text: string + + @IsNumber() + UserId: number + + @IsNumber() + ChannelId: number +} diff --git a/back/volume/src/chat/dto/createChannel.dto.ts b/back/volume/src/chat/dto/createChannel.dto.ts deleted file mode 100644 index cc3fbff..0000000 --- a/back/volume/src/chat/dto/createChannel.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsPositive, IsAlpha, IsString, IsOptional } from 'class-validator' - -export class CreateChannelDto { - @IsString() - @IsAlpha() - name: string - - @IsPositive() - owner: number - - @IsOptional() - password: string -} diff --git a/back/volume/src/chat/dto/update-channel.dto.ts b/back/volume/src/chat/dto/update-channel.dto.ts new file mode 100644 index 0000000..c9cb3a3 --- /dev/null +++ b/back/volume/src/chat/dto/update-channel.dto.ts @@ -0,0 +1,30 @@ +import { PartialType } from '@nestjs/mapped-types' +import { CreateChannelDto } from './create-channel.dto' +import { IsNumber, IsOptional, IsString } from 'class-validator' + +export class UpdateChannelDto extends PartialType(CreateChannelDto) { + id: number + @IsOptional() + @IsNumber() + users: [number] + + @IsOptional() + @IsNumber() + messages: [number] + + @IsOptional() + @IsNumber() + owners: [number] // user id + + @IsOptional() + @IsNumber() + banned: [number] // user id + + @IsOptional() + @IsNumber() + muted: [number] // user id + + @IsString() + @IsOptional() + password: string +} diff --git a/back/volume/src/chat/dto/updateChannel.dto.ts b/back/volume/src/chat/dto/updateChannel.dto.ts deleted file mode 100644 index dc2b5ff..0000000 --- a/back/volume/src/chat/dto/updateChannel.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types' -import { CreateChannelDto } from './createChannel.dto' -import { type Message } from '../entity/message.entity' -import { type User } from 'src/users/entity/user.entity' -import { IsString } from 'class-validator' - -export class UpdateChannelDto extends PartialType(CreateChannelDto) { - id: number - - users: [User] - - messages: [Message] - - owners: [number] // ftId - - admins: [number] - - banned: [number] // ftId - - muted: [number] // ftId - - @IsString() - password: string -} diff --git a/back/volume/src/chat/dto/updateUser.dto.ts b/back/volume/src/chat/dto/updateUser.dto.ts new file mode 100644 index 0000000..bbc0350 --- /dev/null +++ b/back/volume/src/chat/dto/updateUser.dto.ts @@ -0,0 +1,16 @@ + +import { IsNumber, IsString} from 'class-validator' + +export class IdDto { + @IsNumber() + id: number +} + +export class PasswordDto { + @IsString() + password: string +} + +export class MuteDto { + data: Array +} diff --git a/back/volume/src/chat/entity/channel.entity.ts b/back/volume/src/chat/entity/channel.entity.ts index bc06699..6ab26bf 100644 --- a/back/volume/src/chat/entity/channel.entity.ts +++ b/back/volume/src/chat/entity/channel.entity.ts @@ -2,27 +2,40 @@ import { BeforeInsert, Column, Entity, + JoinColumn, JoinTable, ManyToMany, + ManyToOne, OneToMany, + OneToOne, PrimaryGeneratedColumn } from 'typeorm' +import User from 'src/users/entity/user.entity' +import Message from './message.entity' import * as bcrypt from 'bcrypt' -import { User } from 'src/users/entity/user.entity' -import { Message } from './message.entity' - @Entity() -export class Channel { +export default class Channel { @PrimaryGeneratedColumn() id: number @Column() name: string - @ManyToMany(() => User) - @JoinTable() - owner: User + @Column({ default: false }) + isPrivate: boolean + + @Column({ select: false, default: '' }) + password: string + + @BeforeInsert() + async hashPassword () { + if (this.password === '') return + this.password = await bcrypt.hash( + this.password, + Number(process.env.HASH_SALT) + ) + } @ManyToMany(() => User) @JoinTable() @@ -31,22 +44,19 @@ export class Channel { @OneToMany(() => Message, (message: Message) => message.channel) messages: Message[] - @OneToMany(() => User, (user: User) => user.id) // refuse connection - banned: User[] + @ManyToOne(() => User) + @JoinColumn() + owner: User - @OneToMany(() => User, (user: User) => user.id) // refuse post - muted: User[] + @ManyToMany(() => User) + @JoinTable() + admins: User[] - @Column({ select: false }) - password: string + @ManyToMany(() => User) // refuse connection + @JoinTable() + banned: User[] - @BeforeInsert() - async hashPassword () { - this.password = await bcrypt.hash( - this.password, - Number(process.env.HASH_SALT) - ) - } + @ManyToMany(() => User) // refuse post + @JoinTable() + muted: Array> } - -export default Channel diff --git a/back/volume/src/chat/entity/connection.entity.ts b/back/volume/src/chat/entity/connection.entity.ts new file mode 100644 index 0000000..fc949f9 --- /dev/null +++ b/back/volume/src/chat/entity/connection.entity.ts @@ -0,0 +1,16 @@ +import { Column, Entity, OneToOne } from 'typeorm' + +import Channel from './channel.entity' +import User from 'src/users/entity/user.entity' + +@Entity() +export class connectedUser { + @OneToOne(() => User) + user: User + + @OneToOne(() => Channel, (channel) => channel.id) + channel: Channel + + @Column() + socket: string +} diff --git a/back/volume/src/chat/entity/dm.entity.ts b/back/volume/src/chat/entity/dm.entity.ts new file mode 100644 index 0000000..d1a344c --- /dev/null +++ b/back/volume/src/chat/entity/dm.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm' +import Message from './message.entity' +import type User from 'src/users/entity/user.entity' + +@Entity() +export class Channel { + @PrimaryGeneratedColumn() + id: number + + @Column() + users: User[] + + @OneToMany(() => Message, (message) => message.channel) + messages: Message[] +} diff --git a/back/volume/src/chat/entity/message.entity.ts b/back/volume/src/chat/entity/message.entity.ts index 488dacd..e44b4a3 100644 --- a/back/volume/src/chat/entity/message.entity.ts +++ b/back/volume/src/chat/entity/message.entity.ts @@ -7,28 +7,25 @@ import { ManyToOne, PrimaryGeneratedColumn } from 'typeorm' - -import { User } from 'src/users/entity/user.entity' -import { Channel } from './channel.entity' +import User from 'src/users/entity/user.entity' +import Channel from './channel.entity' @Entity() -export class Message { +export default class Message { @PrimaryGeneratedColumn() id: number @Column() text: string - @ManyToOne(() => User, (author: User) => author.messages) + @ManyToOne(() => User) @JoinColumn() author: User - @ManyToOne(() => Channel, (channel: Channel) => channel.messages) + @ManyToOne(() => Channel, (channel) => channel.messages, { cascade: true }) @JoinTable() channel: Channel @CreateDateColumn() - createdAt: Date + created_at: Date } - -export default Message diff --git a/back/volume/src/pong/game/Ball.ts b/back/volume/src/pong/game/Ball.ts index fe5f76f..544f075 100644 --- a/back/volume/src/pong/game/Ball.ts +++ b/back/volume/src/pong/game/Ball.ts @@ -20,7 +20,7 @@ export class Ball { constructor ( spawn: Point, size: Point = DEFAULT_BALL_SIZE, - speed: Point = DEFAULT_BALL_INITIAL_SPEED + speed: Point = DEFAULT_BALL_INITIAL_SPEED.clone() ) { this.rect = new Rect(spawn, size) this.speed = speed @@ -93,21 +93,23 @@ export class Ball { playerScored (): number { let indexPlayerScored: number + if (this.rect.center.x <= this.spawn.x) { indexPlayerScored = 1 + this.speed.x = this.initial_speed.x } else { indexPlayerScored = 0 + this.speed.x = -this.initial_speed.x } - this.rect.center = this.spawn.clone() - if (this.speed.x < 0) { - this.speed.x = this.initial_speed.x - this.speed.y = -this.initial_speed.y - } else { - this.speed.x = -this.initial_speed.x + if (this.speed.y < 0) { this.speed.y = this.initial_speed.y + } else { + this.speed.y = -this.initial_speed.y } + this.rect.center = this.spawn.clone() + return indexPlayerScored } } diff --git a/back/volume/src/pong/game/Game.ts b/back/volume/src/pong/game/Game.ts index 4336be4..7c3d3ee 100644 --- a/back/volume/src/pong/game/Game.ts +++ b/back/volume/src/pong/game/Game.ts @@ -21,7 +21,6 @@ export class Game { map: MapDtoValidated ball: Ball players: Player[] = [] - playing: boolean ranked: boolean waitingForTimeout: boolean gameStoppedCallback: (name: string) => void @@ -37,7 +36,6 @@ export class Game { ) { this.id = randomUUID() this.timer = null - this.playing = false this.ranked = ranked this.waitingForTimeout = false this.map = map @@ -99,7 +97,6 @@ export class Game { void this.pongService.setInGame(p.name) p.newGame() }) - this.playing = true this.broadcastGame(GAME_EVENTS.START_GAME) this.timer = setInterval(this.gameLoop.bind(this), 1000 / GAME_TICKS) console.log(`Game ${this.id} starting in 3 seconds`) @@ -111,21 +108,20 @@ export class Game { } stop (): void { - if (this.timer !== null && this.playing) { - this.playing = false + if (this.timer !== null) { clearInterval(this.timer) - this.timer = null - this.pongService - .saveResult(this.players, this.ranked) - .then(() => { - this.gameStoppedCallback(this.players[0].name) - this.players = [] - }) - .catch(() => { - this.gameStoppedCallback(this.players[0].name) - this.players = [] - }) } + this.timer = null + this.pongService + .saveResult(this.players, this.ranked, DEFAULT_WIN_SCORE) + .then(() => { + this.gameStoppedCallback(this.players[0].name) + this.players = [] + }) + .catch(() => { + this.gameStoppedCallback(this.players[0].name) + this.players = [] + }) } movePaddle (name: string | undefined, position: Point): void { @@ -142,10 +138,6 @@ export class Game { }) } - isPlaying (): boolean { - return this.playing - } - private gameLoop (): void { if (this.waitingForTimeout) { return diff --git a/back/volume/src/pong/pong.controller.ts b/back/volume/src/pong/pong.controller.ts new file mode 100644 index 0000000..809880d --- /dev/null +++ b/back/volume/src/pong/pong.controller.ts @@ -0,0 +1,33 @@ +import { + Controller, + Get, + Param, + ParseIntPipe, + UseGuards +} from '@nestjs/common' +import { Paginate, type Paginated, PaginateQuery } from 'nestjs-paginate' +import { AuthenticatedGuard } from 'src/auth/42-auth.guard' +import type Result from './entity/result.entity' +import { PongService } from './pong.service' + +@Controller('results') +export class PongController { + constructor (private readonly pongService: PongService) {} + + @Get('global') + @UseGuards(AuthenticatedGuard) + async getGlobalHistory ( + @Paginate() query: PaginateQuery + ): Promise> { + return await this.pongService.getHistory(query, 0) + } + + @Get(':id') + @UseGuards(AuthenticatedGuard) + async getHistoryById ( + @Param('id', ParseIntPipe) id: number, + @Paginate() query: PaginateQuery + ): Promise> { + return await this.pongService.getHistory(query, id) + } +} diff --git a/back/volume/src/pong/pong.gateway.ts b/back/volume/src/pong/pong.gateway.ts index 99c3e1d..f2df12b 100644 --- a/back/volume/src/pong/pong.gateway.ts +++ b/back/volume/src/pong/pong.gateway.ts @@ -46,7 +46,7 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { ): void { const name: string | undefined = this.socketToPlayerName.get(client) const game: Game | undefined = this.games.playerGame(name) - if (game?.isPlaying() !== undefined) { + if (game !== undefined) { game.stop() } if (name !== undefined) { diff --git a/back/volume/src/pong/pong.module.ts b/back/volume/src/pong/pong.module.ts index 821acbb..df862a7 100644 --- a/back/volume/src/pong/pong.module.ts +++ b/back/volume/src/pong/pong.module.ts @@ -4,10 +4,12 @@ import Result from './entity/result.entity' import { TypeOrmModule } from '@nestjs/typeorm' import { PongService } from './pong.service' import { UsersModule } from 'src/users/users.module' +import { PongController } from './pong.controller' @Module({ imports: [forwardRef(() => UsersModule), TypeOrmModule.forFeature([Result])], providers: [PongGateway, PongService], + controllers: [PongController], exports: [PongService] }) export class PongModule {} diff --git a/back/volume/src/pong/pong.service.ts b/back/volume/src/pong/pong.service.ts index d024c07..2c59d63 100644 --- a/back/volume/src/pong/pong.service.ts +++ b/back/volume/src/pong/pong.service.ts @@ -15,18 +15,17 @@ export class PongService { private readonly usersService: UsersService ) {} - async updateStats (player: User, i: number, result: Result): Promise { + async updateStats (player: User, i: number, result: Result, maxScore: number): Promise { player.matchs++ - if (result.score[i] > result.score[Math.abs(i - 1)]) player.wins++ + if (result.score[i] === maxScore) player.wins++ else player.looses++ player.winrate = (100 * player.wins) / player.matchs - player.rank = (await this.usersService.getRank(player.ftId)) + 1 } - async updatePlayer (i: number, result: Result): Promise { + async updatePlayer (i: number, result: Result, maxScore: number): Promise { const player: User | null = result.players[i] if (player == null) return - if (result.ranked) await this.updateStats(player, i, result) + if (result.ranked) await this.updateStats(player, i, result, maxScore) player.results.push(result) player.status = 'online' await this.usersService.save(player) @@ -38,7 +37,7 @@ export class PongService { await this.usersService.save(player) } - async saveResult (players: Player[], ranked: boolean): Promise { + async saveResult (players: Player[], ranked: boolean, maxScore: number): Promise { const result = new Result() const ply = new Array() ply.push(await this.usersService.findUserByName(players[0].name)) @@ -47,8 +46,8 @@ export class PongService { result.players = ply result.score = [players[0].score, players[1].score] await this.resultsRepository.save(result) - await this.updatePlayer(0, result) - await this.updatePlayer(1, result) + await this.updatePlayer(0, result, maxScore) + await this.updatePlayer(1, result, maxScore) } async getHistory ( diff --git a/back/volume/src/users/entity/user.entity.ts b/back/volume/src/users/entity/user.entity.ts index 6a8e5ec..a52ce7f 100644 --- a/back/volume/src/users/entity/user.entity.ts +++ b/back/volume/src/users/entity/user.entity.ts @@ -65,12 +65,6 @@ export class User { @JoinTable() results: Result[] - @OneToMany(() => Message, (message: Message) => message.author) - messages: Message[] - - @ManyToMany(() => Channel, (channel: Channel) => channel.users) - rooms: Channel[] - @ManyToMany(() => User) @JoinTable() blocked: User[] diff --git a/back/volume/src/users/users.controller.ts b/back/volume/src/users/users.controller.ts index 5899424..a4b04a5 100644 --- a/back/volume/src/users/users.controller.ts +++ b/back/volume/src/users/users.controller.ts @@ -13,7 +13,6 @@ import { BadRequestException, Redirect } from '@nestjs/common' -import { PaginateQuery, type Paginated, Paginate } from 'nestjs-paginate' import { FileInterceptor } from '@nestjs/platform-express' import { diskStorage } from 'multer' @@ -31,13 +30,11 @@ import { ApiBody, ApiConsumes } from '@nestjs/swagger' import { type Request, Response } from 'express' import { createReadStream } from 'fs' import { join } from 'path' -import type Result from 'src/pong/entity/result.entity' -@Controller() +@Controller("users") export class UsersController { constructor ( private readonly usersService: UsersService, - private readonly pongService: PongService ) {} @Get('all') @@ -68,29 +65,11 @@ export class UsersController { return await this.usersService.getLeaderboard() } - @Get('rank/:id') + @Get(':id/rank') @UseGuards(AuthenticatedGuard) async getRank (@Param('id', ParseIntPipe) id: number): Promise { return await this.usersService.getRank(id) } - - @Get('globalHistory') - @UseGuards(AuthenticatedGuard) - async getGlobalHistory ( - @Paginate() query: PaginateQuery - ): Promise> { - return await this.pongService.getHistory(query, 0) - } - - @Get('history/:id') - @UseGuards(AuthenticatedGuard) - async getHistoryById ( - @Param('id', ParseIntPipe) id: number, - @Paginate() query: PaginateQuery - ): Promise> { - return await this.pongService.getHistory(query, id) - } - @Post('avatar') @UseGuards(AuthenticatedGuard) @Redirect('http://localhost') @@ -130,7 +109,7 @@ export class UsersController { return await this.getAvatarById(profile.id, response) } - @Get('user/:name') + @Get(':name/byname') async getUserByName (@Param('name') username: string): Promise { const user = await this.usersService.findUserByName(username) user.socketKey = '' @@ -140,7 +119,7 @@ export class UsersController { @Get('invit/:username') @UseGuards(AuthenticatedGuard) async invitUser ( - @Profile42() profile: Profile, + @Profile42() profile: Profile, @Param('username') username: string ): Promise { const target: User | null = await this.usersService.findUserByName( @@ -149,14 +128,14 @@ export class UsersController { if (target === null) { throw new BadRequestException(`User ${username} not found.`) } - if (+profile.id === target.ftId) { + if (+profile.id === +target.ftId) { throw new BadRequestException("You can't invite yourself.") } const ret: string = await this.usersService.invit(profile.id, target.ftId) if (ret !== 'OK') throw new BadRequestException(ret) } - @Get('avatar/:id') + @Get(':id/avatar') async getAvatarById ( @Param('id', ParseIntPipe) ftId: number, @Res({ passthrough: true }) response: Response diff --git a/back/volume/src/users/users.service.ts b/back/volume/src/users/users.service.ts index 8f4c4f7..858d86f 100644 --- a/back/volume/src/users/users.service.ts +++ b/back/volume/src/users/users.service.ts @@ -1,12 +1,13 @@ import { BadRequestException, Catch, Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { EntityNotFoundError, QueryFailedError, Repository } from 'typeorm' -import { User } from './entity/user.entity' -import { type UserDto } from './dto/user.dto' -import { type Channel } from 'src/chat/entity/channel.entity' import { Cron } from '@nestjs/schedule' import { randomUUID } from 'crypto' +import { type UserDto } from './dto/user.dto' +import type Channel from 'src/chat/entity/channel.entity' +import User from './entity/user.entity' + @Injectable() @Catch(QueryFailedError, EntityNotFoundError) export class UsersService { @@ -20,9 +21,7 @@ export class UsersService { async findUsers (): Promise { const users = await this.usersRepository.find({}) - users.forEach((usr) => { - usr.socketKey = '' - }) + users.forEach((usr) => usr.socketKey = '') return users } @@ -32,6 +31,7 @@ export class UsersService { relations: { results: true } }) if (user == null) throw new BadRequestException('User not found.') + user.rank = (await this.getRank(user.ftId)) + 1; return user } @@ -42,9 +42,7 @@ export class UsersService { if (Date.now() - usr.lastAccess > 60000) { usr.isVerified = false usr.status = 'offline' - this.usersRepository.save(usr).catch((err) => { - console.log(err) - }) + this.usersRepository.save(usr).catch((err) => console.log(err)) } }) } @@ -52,6 +50,7 @@ export class UsersService { async findUser (ftId: number): Promise { const user = await this.usersRepository.findOneBy({ ftId }) if (user == null) return null + user.rank = (await this.getRank(user.ftId)) + 1; user.lastAccess = Date.now() if (user.status === 'offline') user.status = 'online' await this.usersRepository.save(user) @@ -62,9 +61,7 @@ export class UsersService { const users = await this.usersRepository.find({ where: { status: 'online' } }) - users.forEach((usr) => { - usr.socketKey = '' - }) + users.forEach((usr) => usr.socketKey = '') return users } @@ -101,6 +98,7 @@ export class UsersService { relations: { friends: true } }) if (user == null) throw new BadRequestException('User not found.') + user.friends.forEach((friend) => friend.socketKey = '') return user.friends } @@ -112,6 +110,7 @@ export class UsersService { } }) if (user == null) throw new BadRequestException('User not found.') + user.followers.forEach((follower) => follower.socketKey = '') return user.followers } @@ -121,7 +120,9 @@ export class UsersService { winrate: 'DESC' } }) - return leaderboard.filter((user) => user.rank !== 0) + let ret = leaderboard.filter((user) => user.rank !== 0) + ret.forEach((follower) => follower.socketKey = '') + return ret } async getRank (ftId: number): Promise { diff --git a/front/volume/src/App.svelte b/front/volume/src/App.svelte index 47d5df2..cd979b8 100644 --- a/front/volume/src/App.svelte +++ b/front/volume/src/App.svelte @@ -61,7 +61,7 @@ setAppState(APPSTATE.PROFILE); } - let profileUsername: string; + let profileUsername: string = ""; async function openIdProfile(event: CustomEvent) { profileUsername = event.detail; setAppState(APPSTATE.PROFILE_ID); @@ -71,10 +71,6 @@ setAppState(APPSTATE.HISTORY); } - async function openIdHistory() { - setAppState(APPSTATE.HISTORY_ID); - } - async function clickFriends() { setAppState(APPSTATE.FRIENDS); } @@ -86,7 +82,6 @@ function clickChannels() { setAppState(APPSTATE.CHANNELS); } - let channels: Array = []; let selectedChannel: ChannelsType; const handleSelectChannel = (channel: ChannelsType) => { selectedChannel = channel; @@ -148,7 +143,7 @@ {:else}
- +
{/if} {/if} @@ -177,7 +172,7 @@ {/if} {#if appState === APPSTATE.PROFILE}
- + setAppState(APPSTATE.HISTORY_ID)} />
{/if} {#if appState === APPSTATE.PROFILE_ID} @@ -187,7 +182,7 @@ on:keydown={() => setAppState(APPSTATE.CHANNELS + "#" + selectedChannel.name)} > - + setAppState(APPSTATE.HISTORY_ID)} /> {/if} diff --git a/front/volume/src/Auth.ts b/front/volume/src/Auth.ts index 0cd6919..137f52d 100644 --- a/front/volume/src/Auth.ts +++ b/front/volume/src/Auth.ts @@ -11,8 +11,8 @@ export const API_URL = `http://${import.meta.env.VITE_HOST}:${ import.meta.env.VITE_BACK_PORT }`; -export async function getUser() { - const res = await fetch(API_URL, { +export async function getUser(bypass: boolean = false) { + const res = await fetch(API_URL + "/users", { method: "get", mode: "cors", cache: "no-cache", diff --git a/front/volume/src/FakeLogin.svelte b/front/volume/src/FakeLogin.svelte index a05f2b7..2f76ef5 100644 --- a/front/volume/src/FakeLogin.svelte +++ b/front/volume/src/FakeLogin.svelte @@ -5,7 +5,7 @@ export let ftId; async function doPost() { - await fetch(API_URL + "/" + ftId, { + await fetch(API_URL + "/users/" + ftId, { method: "POST", credentials: "include", mode: "cors", diff --git a/front/volume/src/components/Channels.svelte b/front/volume/src/components/Channels.svelte index 058ff5f..cdee9b0 100644 --- a/front/volume/src/components/Channels.svelte +++ b/front/volume/src/components/Channels.svelte @@ -1,34 +1,31 @@