Browse Source

fixed format

master
nicolas-arnaud 2 years ago
parent
commit
4bb10ffb27
  1. 25
      .env_sample
  2. 16
      back/volume/.eslintrc.js
  3. 44
      back/volume/src/app.module.ts
  4. 26
      back/volume/src/auth/42-auth.guard.ts
  5. 10
      back/volume/src/auth/42.decorator.ts
  6. 58
      back/volume/src/auth/42.strategy.ts
  7. 72
      back/volume/src/auth/auth.controller.ts
  8. 52
      back/volume/src/auth/auth.module.ts
  9. 54
      back/volume/src/auth/auth.service.ts
  10. 14
      back/volume/src/auth/session.serializer.ts
  11. 188
      back/volume/src/chat/chat.controller.ts
  12. 106
      back/volume/src/chat/chat.gateway.ts
  13. 26
      back/volume/src/chat/chat.module.ts
  14. 186
      back/volume/src/chat/chat.service.ts
  15. 8
      back/volume/src/chat/dto/connection.dto.ts
  16. 18
      back/volume/src/chat/dto/create-channel.dto.ts
  17. 8
      back/volume/src/chat/dto/create-message.dto.ts
  18. 20
      back/volume/src/chat/dto/update-channel.dto.ts
  19. 8
      back/volume/src/chat/dto/updateUser.dto.ts
  20. 40
      back/volume/src/chat/entity/channel.entity.ts
  21. 14
      back/volume/src/chat/entity/connection.entity.ts
  22. 12
      back/volume/src/chat/entity/dm.entity.ts
  23. 18
      back/volume/src/chat/entity/message.entity.ts
  24. 46
      back/volume/src/chat/message.service.ts
  25. 56
      back/volume/src/main.ts
  26. 20
      back/volume/src/pong/dtos/GameCreationDtoValidated.ts
  27. 18
      back/volume/src/pong/dtos/GameInfo.ts
  28. 10
      back/volume/src/pong/dtos/GameUpdate.ts
  29. 14
      back/volume/src/pong/dtos/MapDtoValidated.ts
  30. 2
      back/volume/src/pong/dtos/MatchmakingDto.ts
  31. 6
      back/volume/src/pong/dtos/MatchmakingDtoValidated.ts
  32. 8
      back/volume/src/pong/dtos/PointDtoValidated.ts
  33. 12
      back/volume/src/pong/dtos/RectDtoValidated.ts
  34. 2
      back/volume/src/pong/dtos/StringDto.ts
  35. 6
      back/volume/src/pong/dtos/StringDtoValidated.ts
  36. 8
      back/volume/src/pong/dtos/UserDto.ts
  37. 18
      back/volume/src/pong/entity/result.entity.ts
  38. 106
      back/volume/src/pong/game/Ball.ts
  39. 158
      back/volume/src/pong/game/Game.ts
  40. 96
      back/volume/src/pong/game/Games.ts
  41. 58
      back/volume/src/pong/game/MatchmakingQueue.ts
  42. 30
      back/volume/src/pong/game/Paddle.ts
  43. 46
      back/volume/src/pong/game/Player.ts
  44. 38
      back/volume/src/pong/game/constants.ts
  45. 72
      back/volume/src/pong/game/utils.ts
  46. 30
      back/volume/src/pong/pong.controller.ts
  47. 24
      back/volume/src/pong/pong.gateway.spec.ts
  48. 158
      back/volume/src/pong/pong.gateway.ts
  49. 16
      back/volume/src/pong/pong.module.ts
  50. 96
      back/volume/src/pong/pong.service.ts
  51. 24
      back/volume/src/pong/pong.spec.ts
  52. 10
      back/volume/src/types.d.ts
  53. 22
      back/volume/src/users/dto/user.dto.ts
  54. 62
      back/volume/src/users/entity/user.entity.ts
  55. 198
      back/volume/src/users/users.controller.ts
  56. 16
      back/volume/src/users/users.module.ts
  57. 210
      back/volume/src/users/users.service.ts
  58. 312
      front/volume/src/components/Chat.svelte

25
.env_sample

@ -0,0 +1,25 @@
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=postgres_usr
POSTGRES_PASSWORD=postgres_pw
POSTGRES_DB=transcendence
PGADMIN_DEFAULT_EMAIL=admin@pg.com
PGADMIN_DEFAULT_PASSWORD=admin
MAIL_USER=vaganiwast@gmail.com
MAIL_PASSWORD=
FRONT_FPS=144
HOST=localhost
FRONT_PORT=80
BACK_PORT=3001
HASH_SALT=10
JWT_SECRET=test
JWT_EXPIRATION_TIME=900
FT_OAUTH_CLIENT_ID=
FT_OAUTH_CLIENT_SECRET=
FT_OAUTH_CALLBACK_URL=http://$HOST:$BACK_PORT/log/inReturn

16
back/volume/.eslintrc.js

@ -1,13 +1,13 @@
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true,
es2021: true, es2021: true
}, },
extends: "standard-with-typescript", extends: 'standard-with-typescript',
parserOptions: { parserOptions: {
ecmaVersion: "latest", ecmaVersion: 'latest',
sourceType: "module", sourceType: 'module',
project: ["./tsconfig.json"], project: ['./tsconfig.json'],
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname
}, }
}; }

44
back/volume/src/app.module.ts

@ -1,13 +1,13 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from "@nestjs/config"; import { ConfigModule, ConfigService } from '@nestjs/config'
import { TypeOrmModule } from "@nestjs/typeorm"; import { TypeOrmModule } from '@nestjs/typeorm'
import * as Joi from "joi"; import * as Joi from 'joi'
import { ScheduleModule } from "@nestjs/schedule"; import { ScheduleModule } from '@nestjs/schedule'
import { AuthModule } from "./auth/auth.module"; import { AuthModule } from './auth/auth.module'
import { ChatModule } from "./chat/chat.module"; import { ChatModule } from './chat/chat.module'
import { PongModule } from "./pong/pong.module"; import { PongModule } from './pong/pong.module'
import { UsersModule } from "./users/users.module"; import { UsersModule } from './users/users.module'
@Module({ @Module({
imports: [ imports: [
@ -26,28 +26,28 @@ import { UsersModule } from "./users/users.module";
HOST: Joi.string().required(), HOST: Joi.string().required(),
FRONT_PORT: Joi.number().required(), FRONT_PORT: Joi.number().required(),
BACK_PORT: Joi.number().required(), BACK_PORT: Joi.number().required(),
HASH_SALT: Joi.number().required(), HASH_SALT: Joi.number().required()
}), })
}), }),
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: (configService: ConfigService) => ({ useFactory: (configService: ConfigService) => ({
type: "postgres", type: 'postgres',
host: configService.get<string>("POSTGRES_HOST"), host: configService.get<string>('POSTGRES_HOST'),
port: configService.get<number>("POSTGRES_PORT"), port: configService.get<number>('POSTGRES_PORT'),
username: configService.get<string>("POSTGRES_USER"), username: configService.get<string>('POSTGRES_USER'),
password: configService.get<string>("POSTGRES_PASSWORD"), password: configService.get<string>('POSTGRES_PASSWORD'),
database: configService.get<string>("POSTGRES_DB"), database: configService.get<string>('POSTGRES_DB'),
jwt_secret: configService.get<string>("JWT_SECRET"), jwt_secret: configService.get<string>('JWT_SECRET'),
autoLoadEntities: true, autoLoadEntities: true,
synchronize: true, synchronize: true
}), })
}), }),
AuthModule, AuthModule,
ChatModule, ChatModule,
PongModule, PongModule,
UsersModule, UsersModule
], ]
}) })
export class AppModule {} export class AppModule {}

26
back/volume/src/auth/42-auth.guard.ts

@ -1,25 +1,25 @@
import { import {
type ExecutionContext, type ExecutionContext,
Injectable, Injectable,
type CanActivate, type CanActivate
} from "@nestjs/common"; } from '@nestjs/common'
import { AuthGuard } from "@nestjs/passport"; import { AuthGuard } from '@nestjs/passport'
import { type Request } from "express"; import { type Request } from 'express'
@Injectable() @Injectable()
export class FtOauthGuard extends AuthGuard("42") { export class FtOauthGuard extends AuthGuard('42') {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate (context: ExecutionContext): Promise<boolean> {
const activate: boolean = (await super.canActivate(context)) as boolean; const activate: boolean = (await super.canActivate(context)) as boolean
const request: Request = context.switchToHttp().getRequest(); const request: Request = context.switchToHttp().getRequest()
await super.logIn(request); await super.logIn(request)
return activate; return activate
} }
} }
@Injectable() @Injectable()
export class AuthenticatedGuard implements CanActivate { export class AuthenticatedGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate (context: ExecutionContext): Promise<boolean> {
const req: Request = context.switchToHttp().getRequest(); const req: Request = context.switchToHttp().getRequest()
return req.isAuthenticated(); return req.isAuthenticated()
} }
} }

10
back/volume/src/auth/42.decorator.ts

@ -1,9 +1,9 @@
import { createParamDecorator, type ExecutionContext } from "@nestjs/common"; import { createParamDecorator, type ExecutionContext } from '@nestjs/common'
import { type Profile } from "passport-42"; import { type Profile } from 'passport-42'
export const Profile42 = createParamDecorator( export const Profile42 = createParamDecorator(
(data: unknown, ctx: ExecutionContext): Profile => { (data: unknown, ctx: ExecutionContext): Profile => {
const request = ctx.switchToHttp().getRequest(); const request = ctx.switchToHttp().getRequest()
return request.user; return request.user
} }
); )

58
back/volume/src/auth/42.strategy.ts

@ -1,49 +1,49 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common'
import { ConfigService } from "@nestjs/config"; import { ConfigService } from '@nestjs/config'
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from '@nestjs/passport'
import { Strategy, type Profile, type VerifyCallback } from "passport-42"; import { Strategy, type Profile, type VerifyCallback } from 'passport-42'
import { get } from "https"; import { get } from 'https'
import { createWriteStream } from "fs"; import { createWriteStream } from 'fs'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
import { User } from "src/users/entity/user.entity"; import { User } from 'src/users/entity/user.entity'
@Injectable() @Injectable()
export class FtStrategy extends PassportStrategy(Strategy, "42") { export class FtStrategy extends PassportStrategy(Strategy, '42') {
constructor( constructor (
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly usersService: UsersService private readonly usersService: UsersService
) { ) {
super({ super({
clientID: configService.get<string>("FT_OAUTH_CLIENT_ID"), clientID: configService.get<string>('FT_OAUTH_CLIENT_ID'),
clientSecret: configService.get<string>("FT_OAUTH_CLIENT_SECRET"), clientSecret: configService.get<string>('FT_OAUTH_CLIENT_SECRET'),
callbackURL: configService.get<string>("FT_OAUTH_CALLBACK_URL"), callbackURL: configService.get<string>('FT_OAUTH_CALLBACK_URL'),
passReqToCallback: true, passReqToCallback: true
}); })
} }
async validate( async validate (
request: { session: { accessToken: string } }, request: { session: { accessToken: string } },
accessToken: string, accessToken: string,
refreshToken: string, refreshToken: string,
profile: Profile, profile: Profile,
cb: VerifyCallback cb: VerifyCallback
): Promise<VerifyCallback> { ): Promise<VerifyCallback> {
request.session.accessToken = accessToken; request.session.accessToken = accessToken
const ftId = profile.id as number; const ftId = profile.id as number
console.log("Validated ", profile.username); console.log('Validated ', profile.username)
if ((await this.usersService.findUser(ftId)) === null) { if ((await this.usersService.findUser(ftId)) === null) {
const newUser = new User(); const newUser = new User()
newUser.ftId = profile.id as number; newUser.ftId = profile.id as number
newUser.username = profile.username; newUser.username = profile.username
newUser.avatar = `${ftId}.jpg`; newUser.avatar = `${ftId}.jpg`
newUser.email = profile.emails[0].value; newUser.email = profile.emails[0].value
void this.usersService.create(newUser); void this.usersService.create(newUser)
const file = createWriteStream(`avatars/${ftId}.jpg`); const file = createWriteStream(`avatars/${ftId}.jpg`)
get(profile._json.image.versions.small, function (response) { get(profile._json.image.versions.small, function (response) {
response.pipe(file); response.pipe(file)
}); })
} }
return cb(null, profile); return cb(null, profile)
} }
} }

72
back/volume/src/auth/auth.controller.ts

@ -6,75 +6,75 @@ import {
Res, Res,
Req, Req,
Post, Post,
Body, Body
} from "@nestjs/common"; } from '@nestjs/common'
import { Response, Request } from "express"; import { Response, Request } from 'express'
import { FtOauthGuard, AuthenticatedGuard } from "./42-auth.guard"; import { FtOauthGuard, AuthenticatedGuard } from './42-auth.guard'
import { Profile } from "passport-42"; import { Profile } from 'passport-42'
import { Profile42 } from "./42.decorator"; import { Profile42 } from './42.decorator'
import { AuthService } from "./auth.service"; import { AuthService } from './auth.service'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
const frontHost = const frontHost =
process.env.HOST !== undefined && process.env.HOST !== "" process.env.HOST !== undefined && process.env.HOST !== ''
? process.env.HOST ? process.env.HOST
: "localhost"; : 'localhost'
const frontPort = const frontPort =
process.env.PORT !== undefined && process.env.HOST !== "" process.env.PORT !== undefined && process.env.HOST !== ''
? process.env.PORT ? process.env.PORT
: "80"; : '80'
@Controller("log") @Controller('log')
export class AuthController { export class AuthController {
constructor( constructor (
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly usersService: UsersService private readonly usersService: UsersService
) {} ) {}
@Get("in") @Get('in')
@UseGuards(FtOauthGuard) @UseGuards(FtOauthGuard)
ftAuth(): void {} ftAuth (): void {}
@Get("inReturn") @Get('inReturn')
@UseGuards(FtOauthGuard) @UseGuards(FtOauthGuard)
@Redirect(`http://${frontHost}:${frontPort}`) @Redirect(`http://${frontHost}:${frontPort}`)
ftAuthCallback( ftAuthCallback (
@Res({ passthrough: true }) response: Response, @Res({ passthrough: true }) response: Response,
@Req() request: Request @Req() request: Request
): any { ): any {
console.log("cookie:", request.cookies["connect.sid"]); console.log('cookie:', request.cookies['connect.sid'])
response.cookie("connect.sid", request.cookies["connect.sid"]); response.cookie('connect.sid', request.cookies['connect.sid'])
} }
@Get("/verify") @Get('/verify')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
@Redirect(`http://${frontHost}:${frontPort}`) @Redirect(`http://${frontHost}:${frontPort}`)
async VerifyEmail(@Profile42() profile: Profile): Promise<void> { async VerifyEmail (@Profile42() profile: Profile): Promise<void> {
const ftId: number = profile.id; const ftId: number = profile.id
const user = await this.usersService.findUser(ftId); const user = await this.usersService.findUser(ftId)
if (user == null) throw new Error("User not found"); if (user == null) throw new Error('User not found')
await this.authService.sendConfirmationEmail(user); await this.authService.sendConfirmationEmail(user)
} }
@Post("/verify") @Post('/verify')
@Redirect(`http://${frontHost}:${frontPort}`) @Redirect(`http://${frontHost}:${frontPort}`)
async Verify(@Body() body: any): Promise<void> { async Verify (@Body() body: any): Promise<void> {
await this.authService.verifyAccount(body.code); await this.authService.verifyAccount(body.code)
} }
@Get("profile") @Get('profile')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
profile(@Profile42() user: Profile): any { profile (@Profile42() user: Profile): any {
return { user }; return { user }
} }
@Get("out") @Get('out')
@Redirect(`http://${frontHost}:${frontPort}`) @Redirect(`http://${frontHost}:${frontPort}`)
logOut(@Req() req: Request): any { logOut (@Req() req: Request): any {
req.logOut(function (err) { req.logOut(function (err) {
if (err != null) return err; if (err != null) return err
}); })
} }
} }

52
back/volume/src/auth/auth.module.ts

@ -1,23 +1,23 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common'
import { UsersModule } from "src/users/users.module"; import { UsersModule } from 'src/users/users.module'
import { PassportModule } from "@nestjs/passport"; import { PassportModule } from '@nestjs/passport'
import { ConfigModule, ConfigService } from "@nestjs/config"; import { ConfigModule, ConfigService } from '@nestjs/config'
import { AuthController } from "./auth.controller"; import { AuthController } from './auth.controller'
import { FtStrategy } from "./42.strategy"; import { FtStrategy } from './42.strategy'
import { SessionSerializer } from "./session.serializer"; import { SessionSerializer } from './session.serializer'
import { JwtModule } from "@nestjs/jwt"; import { JwtModule } from '@nestjs/jwt'
import { MailerModule } from "@nestjs-modules/mailer"; import { MailerModule } from '@nestjs-modules/mailer'
import { AuthService } from "./auth.service"; import { AuthService } from './auth.service'
import { HandlebarsAdapter } from "@nestjs-modules/mailer/dist/adapters/handlebars.adapter"; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'
const mailUser = const mailUser =
process.env.MAIL_USER !== null && process.env.MAIL_USER !== "" process.env.MAIL_USER !== null && process.env.MAIL_USER !== ''
? process.env.MAIL_USER ? process.env.MAIL_USER
: ""; : ''
const mailPass = const mailPass =
process.env.MAIL_PASSWORD !== null && process.env.MAIL_PASSWORD !== "" process.env.MAIL_PASSWORD !== null && process.env.MAIL_PASSWORD !== ''
? process.env.MAIL_PASSWORD ? process.env.MAIL_PASSWORD
: ""; : ''
@Module({ @Module({
imports: [ imports: [
@ -26,29 +26,29 @@ const mailPass =
ConfigModule.forRoot(), ConfigModule.forRoot(),
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_SECRET, secret: process.env.JWT_SECRET,
signOptions: { expiresIn: "60s" }, signOptions: { expiresIn: '60s' }
}), }),
MailerModule.forRoot({ MailerModule.forRoot({
transport: { transport: {
service: "gmail", service: 'gmail',
auth: { auth: {
user: mailUser, user: mailUser,
pass: mailPass, pass: mailPass
}, }
}, },
template: { template: {
dir: "src/auth/mails", dir: 'src/auth/mails',
adapter: new HandlebarsAdapter(), adapter: new HandlebarsAdapter(),
options: { options: {
strict: true, strict: true
}, }
}, },
defaults: { defaults: {
from: '"No Reply" vaganiwast@gmail.com', from: '"No Reply" vaganiwast@gmail.com'
}, }
}), })
], ],
providers: [ConfigService, FtStrategy, SessionSerializer, AuthService], providers: [ConfigService, FtStrategy, SessionSerializer, AuthService],
controllers: [AuthController], controllers: [AuthController]
}) })
export class AuthModule {} export class AuthModule {}

54
back/volume/src/auth/auth.service.ts

@ -1,48 +1,48 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common'
import { type User } from "src/users/entity/user.entity"; import { type User } from 'src/users/entity/user.entity'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
import { MailerService } from "@nestjs-modules/mailer"; import { MailerService } from '@nestjs-modules/mailer'
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor (
private readonly usersService: UsersService, private readonly usersService: UsersService,
private readonly mailerService: MailerService private readonly mailerService: MailerService
) {} ) {}
async sendConfirmedEmail(user: User): Promise<void> { async sendConfirmedEmail (user: User): Promise<void> {
const { email, username } = user; const { email, username } = user
await this.mailerService.sendMail({ await this.mailerService.sendMail({
to: email, to: email,
subject: "Welcome to ft_transcendence! Email Confirmed", subject: 'Welcome to ft_transcendence! Email Confirmed',
template: "confirmed", template: 'confirmed',
context: { context: {
username, username,
email, email
}, }
}); })
} }
async sendConfirmationEmail(user: User): Promise<void> { async sendConfirmationEmail (user: User): Promise<void> {
user.authToken = Math.floor(10000 + Math.random() * 90000).toString(); user.authToken = Math.floor(10000 + Math.random() * 90000).toString()
await this.usersService.save(user); await this.usersService.save(user)
await this.mailerService.sendMail({ await this.mailerService.sendMail({
to: user.email, to: user.email,
subject: "Welcome to ft_transcendence! Confirm Email", subject: 'Welcome to ft_transcendence! Confirm Email',
template: "confirm", template: 'confirm',
context: { context: {
username: user.username, username: user.username,
code: user.authToken, code: user.authToken
}, }
}); })
} }
async verifyAccount(code: string): Promise<boolean> { async verifyAccount (code: string): Promise<boolean> {
const user = await this.usersService.findByCode(code); const user = await this.usersService.findByCode(code)
user.authToken = ""; user.authToken = ''
user.isVerified = true; user.isVerified = true
await this.usersService.save(user); await this.usersService.save(user)
await this.sendConfirmedEmail(user); await this.sendConfirmedEmail(user)
return true; return true
} }
} }

14
back/volume/src/auth/session.serializer.ts

@ -1,20 +1,20 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common'
import { PassportSerializer } from "@nestjs/passport"; import { PassportSerializer } from '@nestjs/passport'
import { type Profile } from "passport-42"; import { type Profile } from 'passport-42'
@Injectable() @Injectable()
export class SessionSerializer extends PassportSerializer { export class SessionSerializer extends PassportSerializer {
serializeUser( serializeUser (
user: Profile, user: Profile,
done: (err: Error | null, user: Profile) => void done: (err: Error | null, user: Profile) => void
): any { ): any {
done(null, user); done(null, user)
} }
deserializeUser( deserializeUser (
payload: Profile, payload: Profile,
done: (err: Error | null, user: Profile) => void done: (err: Error | null, user: Profile) => void
): any { ): any {
done(null, payload); done(null, payload)
} }
} }

188
back/volume/src/chat/chat.controller.ts

@ -7,211 +7,211 @@ import {
NotFoundException, NotFoundException,
Param, Param,
Post, Post,
UseGuards, UseGuards
} from "@nestjs/common"; } from '@nestjs/common'
import { AuthenticatedGuard } from "src/auth/42-auth.guard"; import { AuthenticatedGuard } from 'src/auth/42-auth.guard'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
import { ChatService } from "./chat.service"; import { ChatService } from './chat.service'
import { CreateChannelDto } from "./dto/create-channel.dto"; import { CreateChannelDto } from './dto/create-channel.dto'
import { IdDto, PasswordDto, MuteDto } from "./dto/updateUser.dto"; import { IdDto, PasswordDto, MuteDto } from './dto/updateUser.dto'
import type User from "src/users/entity/user.entity"; import type User from 'src/users/entity/user.entity'
import type Channel from "./entity/channel.entity"; import type Channel from './entity/channel.entity'
import { Profile42 } from "src/auth/42.decorator"; import { Profile42 } from 'src/auth/42.decorator'
import { Profile } from "passport-42"; import { Profile } from 'passport-42'
@Controller("channels") @Controller('channels')
export class ChatController { export class ChatController {
constructor( constructor (
private readonly channelService: ChatService, private readonly channelService: ChatService,
private readonly usersService: UsersService private readonly usersService: UsersService
) {} ) {}
@Post(":id/invite") @Post(':id/invite')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async addUser( async addUser (
@Param("id") id: number, @Param('id') id: number,
@Body() target: IdDto, @Body() target: IdDto,
@Profile42() profile: Profile @Profile42() profile: Profile
) { ) {
const channel = await this.channelService.getFullChannel(id); const channel = await this.channelService.getFullChannel(id)
const user: User | null = await this.usersService.findUser(target.id); const user: User | null = await this.usersService.findUser(target.id)
if (user == null) { if (user == null) {
throw new NotFoundException(`User #${target.id} not found`); throw new NotFoundException(`User #${target.id} not found`)
} }
if (!(await this.channelService.isUser(channel.id, +profile.id))) { if (!(await this.channelService.isUser(channel.id, +profile.id))) {
throw new BadRequestException( throw new BadRequestException(
"You are not allowed to invite users to this channel" 'You are not allowed to invite users to this channel'
); )
} }
if (await this.channelService.isUser(channel.id, target.id)) { if (await this.channelService.isUser(channel.id, target.id)) {
throw new BadRequestException("User is already in this channel"); throw new BadRequestException('User is already in this channel')
} }
if (await this.channelService.isBanned(channel.id, target.id)) { if (await this.channelService.isBanned(channel.id, target.id)) {
throw new BadRequestException("User is banned from this channel"); throw new BadRequestException('User is banned from this channel')
} }
channel.users.push(user); channel.users.push(user)
this.channelService.save(channel); this.channelService.save(channel)
} }
@Delete(":id/kick") @Delete(':id/kick')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async removeUser( async removeUser (
@Param("id") id: number, @Param('id') id: number,
@Body() target: IdDto, @Body() target: IdDto,
@Profile42() profile: Profile @Profile42() profile: Profile
) { ) {
const channel = await this.channelService.getFullChannel(id); const channel = await this.channelService.getFullChannel(id)
if (!(await this.channelService.isAdmin(channel.id, +profile.id))) { if (!(await this.channelService.isAdmin(channel.id, +profile.id))) {
throw new BadRequestException( throw new BadRequestException(
"You are not allowed to kick users from this channel" 'You are not allowed to kick users from this channel'
); )
} }
if (!(await this.channelService.isUser(channel.id, target.id))) { if (!(await this.channelService.isUser(channel.id, target.id))) {
throw new BadRequestException("User is not in this channel"); throw new BadRequestException('User is not in this channel')
} }
if (await this.channelService.isOwner(channel.id, target.id)) { if (await this.channelService.isOwner(channel.id, target.id)) {
throw new BadRequestException("You cannot kick the owner of the channel"); throw new BadRequestException('You cannot kick the owner of the channel')
} }
channel.users = channel.users.filter((usr: User) => { channel.users = channel.users.filter((usr: User) => {
return usr.ftId !== target.id; return usr.ftId !== target.id
}); })
this.channelService.save(channel); this.channelService.save(channel)
} }
@Post(":id/admin") @Post(':id/admin')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async addAdmin( async addAdmin (
@Param("id") id: number, @Param('id') id: number,
@Body() target: IdDto, @Body() target: IdDto,
@Profile42() profile: Profile @Profile42() profile: Profile
) { ) {
const channel = await this.channelService.getFullChannel(id); const channel = await this.channelService.getFullChannel(id)
const user: User | null = await this.usersService.findUser(target.id); const user: User | null = await this.usersService.findUser(target.id)
if (user == null) { if (user == null) {
throw new NotFoundException(`User #${target.id} not found`); throw new NotFoundException(`User #${target.id} not found`)
} }
if (!(await this.channelService.isOwner(channel.id, +profile.id))) { if (!(await this.channelService.isOwner(channel.id, +profile.id))) {
throw new BadRequestException("You are not the owner of this channel"); throw new BadRequestException('You are not the owner of this channel')
} }
if (!(await this.channelService.isUser(channel.id, target.id))) { if (!(await this.channelService.isUser(channel.id, target.id))) {
throw new BadRequestException("User is not in this channel"); throw new BadRequestException('User is not in this channel')
} }
if (await this.channelService.isAdmin(channel.id, target.id)) { if (await this.channelService.isAdmin(channel.id, target.id)) {
throw new BadRequestException("User is already an admin of this channel"); throw new BadRequestException('User is already an admin of this channel')
} }
channel.admins.push(user); channel.admins.push(user)
this.channelService.save(channel); this.channelService.save(channel)
} }
@Delete(":id/admin") @Delete(':id/admin')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async removeAdmin( async removeAdmin (
@Param("id") id: number, @Param('id') id: number,
@Body() target: IdDto, @Body() target: IdDto,
@Profile42() profile: Profile @Profile42() profile: Profile
) { ) {
const channel = await this.channelService.getFullChannel(id); const channel = await this.channelService.getFullChannel(id)
if (!(await this.channelService.isOwner(channel.id, +profile.id))) { if (!(await this.channelService.isOwner(channel.id, +profile.id))) {
throw new BadRequestException("You are not the owner of this channel"); throw new BadRequestException('You are not the owner of this channel')
} }
if (!(await this.channelService.isAdmin(channel.id, target.id))) { if (!(await this.channelService.isAdmin(channel.id, target.id))) {
throw new BadRequestException("User is not an admin of this channel"); throw new BadRequestException('User is not an admin of this channel')
} }
channel.admins = channel.admins.filter((usr: User) => { channel.admins = channel.admins.filter((usr: User) => {
return usr.ftId !== target.id; return usr.ftId !== target.id
}); })
this.channelService.save(channel); this.channelService.save(channel)
} }
@Post(":id/ban") @Post(':id/ban')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async addBan( async addBan (
@Param("id") id: number, @Param('id') id: number,
@Body() target: IdDto, @Body() target: IdDto,
@Profile42() profile: Profile @Profile42() profile: Profile
) { ) {
const channel = await this.channelService.getFullChannel(id); const channel = await this.channelService.getFullChannel(id)
const user: User | null = await this.usersService.findUser(target.id); const user: User | null = await this.usersService.findUser(target.id)
if (user == null) { if (user == null) {
throw new NotFoundException(`User #${target.id} not found`); throw new NotFoundException(`User #${target.id} not found`)
} }
if (!(await this.channelService.isAdmin(channel.id, +profile.id))) { if (!(await this.channelService.isAdmin(channel.id, +profile.id))) {
throw new BadRequestException( throw new BadRequestException(
"You are not allowed to ban users from this channel" 'You are not allowed to ban users from this channel'
); )
} }
if (await this.channelService.isOwner(channel.id, target.id)) { if (await this.channelService.isOwner(channel.id, target.id)) {
throw new BadRequestException("You cannot ban the owner of the channel"); throw new BadRequestException('You cannot ban the owner of the channel')
} }
if (await this.channelService.isBanned(channel.id, target.id)) { if (await this.channelService.isBanned(channel.id, target.id)) {
throw new BadRequestException("User is already banned from this channel"); throw new BadRequestException('User is already banned from this channel')
} }
channel.banned.push(user); channel.banned.push(user)
this.channelService.save(channel); this.channelService.save(channel)
} }
@Post(":id/mute") @Post(':id/mute')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async addMute( async addMute (
@Param("id") id: number, @Param('id') id: number,
@Body() mute: MuteDto, // [userId, duration] @Body() mute: MuteDto, // [userId, duration]
@Profile42() profile: Profile @Profile42() profile: Profile
) { ) {
const channel = await this.channelService.getFullChannel(id); const channel = await this.channelService.getFullChannel(id)
const user: User | null = await this.usersService.findUser(mute.data[0]); const user: User | null = await this.usersService.findUser(mute.data[0])
if (user == null) { if (user == null) {
throw new NotFoundException(`User #${mute.data[0]} not found`); throw new NotFoundException(`User #${mute.data[0]} not found`)
} }
if (!(await this.channelService.isAdmin(channel.id, +profile.id))) { if (!(await this.channelService.isAdmin(channel.id, +profile.id))) {
throw new BadRequestException( throw new BadRequestException(
"You are not allowed to mute users from this channel" 'You are not allowed to mute users from this channel'
); )
} }
if (await this.channelService.isOwner(channel.id, mute.data[0])) { if (await this.channelService.isOwner(channel.id, mute.data[0])) {
throw new BadRequestException("You cannot mute the owner of the channel"); throw new BadRequestException('You cannot mute the owner of the channel')
} }
if ( if (
(await this.channelService.getMuteDuration(channel.id, mute.data[0])) > 0 (await this.channelService.getMuteDuration(channel.id, mute.data[0])) > 0
) { ) {
throw new BadRequestException("User is already muted from this channel"); throw new BadRequestException('User is already muted from this channel')
} }
const newMute: number[] = [mute.data[0], Date.now() + mute.data[1] * 1000]; const newMute: number[] = [mute.data[0], Date.now() + mute.data[1] * 1000]
channel.muted.push(newMute); channel.muted.push(newMute)
this.channelService.save(channel); this.channelService.save(channel)
} }
@Delete(":id") @Delete(':id')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async deleteChannel(@Profile42() profile: Profile, @Param("id") id: number) { async deleteChannel (@Profile42() profile: Profile, @Param('id') id: number) {
if (!(await this.channelService.isOwner(id, +profile.id))) { if (!(await this.channelService.isOwner(id, +profile.id))) {
throw new BadRequestException("You are not the owner of this channel"); throw new BadRequestException('You are not the owner of this channel')
} }
await this.channelService.removeChannel(id); await this.channelService.removeChannel(id)
} }
@Post(":id/password") @Post(':id/password')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async updatePassword( async updatePassword (
@Profile42() profile: Profile, @Profile42() profile: Profile,
@Param("id") id: number, @Param('id') id: number,
@Body() data: PasswordDto @Body() data: PasswordDto
) { ) {
if (await this.channelService.isOwner(id, +profile.id)) { if (await this.channelService.isOwner(id, +profile.id)) {
throw new BadRequestException("You are not the owner of this channel"); throw new BadRequestException('You are not the owner of this channel')
} }
await this.channelService.updatePassword(id, data.password); await this.channelService.updatePassword(id, data.password)
} }
@Get() @Get()
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getChannelsForUser(@Profile42() profile: Profile): Promise<Channel[]> { async getChannelsForUser (@Profile42() profile: Profile): Promise<Channel[]> {
return await this.channelService.getChannelsForUser(+profile.id); return await this.channelService.getChannelsForUser(+profile.id)
} }
@Post() @Post()
async createChannel(@Body() channel: CreateChannelDto) { async createChannel (@Body() channel: CreateChannelDto) {
return await this.channelService.createChannel(channel); return await this.channelService.createChannel(channel)
} }
} }

106
back/volume/src/chat/chat.gateway.ts

@ -4,31 +4,31 @@ import {
SubscribeMessage, SubscribeMessage,
WebSocketGateway, WebSocketGateway,
WebSocketServer, WebSocketServer,
WsException, WsException
} from "@nestjs/websockets"; } from '@nestjs/websockets'
import { Socket, Server } from "socket.io"; import { Socket, Server } from 'socket.io'
// import { User } from 'users/user.entity'; // import { User } from 'users/user.entity';
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
import { BadRequestException } from "@nestjs/common"; import { BadRequestException } from '@nestjs/common'
import { ChatService } from "./chat.service"; import { ChatService } from './chat.service'
import type Message from "./entity/message.entity"; import type Message from './entity/message.entity'
import * as bcrypt from "bcrypt"; import * as bcrypt from 'bcrypt'
import { MessageService } from "./message.service"; import { MessageService } from './message.service'
import { type User } from "src/users/entity/user.entity"; import { type User } from 'src/users/entity/user.entity'
import { CreateMessageDto } from "./dto/create-message.dto"; import { CreateMessageDto } from './dto/create-message.dto'
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from "typeorm"; import { Repository } from 'typeorm'
import ConnectedUser from "./entity/connection.entity"; import ConnectedUser from './entity/connection.entity'
import { ConnectionDto } from "./dto/connection.dto"; import { ConnectionDto } from './dto/connection.dto'
@WebSocketGateway({ @WebSocketGateway({
cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ }, cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ }
}) })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() @WebSocketServer()
server: Server; server: Server
constructor( constructor (
private readonly userService: UsersService, private readonly userService: UsersService,
private readonly messageService: MessageService, private readonly messageService: MessageService,
private readonly chatService: ChatService, private readonly chatService: ChatService,
@ -36,7 +36,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly connectedUserRepository: Repository<ConnectedUser> private readonly connectedUserRepository: Repository<ConnectedUser>
) {} ) {}
async handleConnection(socket: Socket): Promise<void> { async handleConnection (socket: Socket): Promise<void> {
// console.log(socket.handshake.headers) // console.log(socket.handshake.headers)
// const cookie = socket.handshake.headers.cookie as string // const cookie = socket.handshake.headers.cookie as string
// const { authentication: authenticationToken } = parse(cookie) // const { authentication: authenticationToken } = parse(cookie)
@ -49,76 +49,76 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
// return user // return user
} }
handleDisconnect(socket: Socket): void { handleDisconnect (socket: Socket): void {
socket.disconnect(); socket.disconnect()
} }
@SubscribeMessage("joinChannel") @SubscribeMessage('joinChannel')
async onJoinChannel(socket: Socket, connect: ConnectionDto): Promise<void> { async onJoinChannel (socket: Socket, connect: ConnectionDto): Promise<void> {
const channel = await this.chatService.getChannel(connect.ChannelId); const channel = await this.chatService.getChannel(connect.ChannelId)
if (channel.banned.find((e) => e.id == connect.UserId) != null) { if (channel.banned.find((e) => e.id == connect.UserId) != null) {
throw new WsException("You are banned from entering this channel"); throw new WsException('You are banned from entering this channel')
} }
const user = (await this.userService.findUser(connect.UserId)) as User; const user = (await this.userService.findUser(connect.UserId)) as User
if ( if (
channel.users.find((e) => e.id === user.id) == null && channel.users.find((e) => e.id === user.id) == null &&
channel.password !== "" channel.password !== ''
) { ) {
if (!(await bcrypt.compare(channel.password, connect.pwd))) { if (!(await bcrypt.compare(channel.password, connect.pwd))) {
throw new BadRequestException(); throw new BadRequestException()
} }
} else await this.chatService.addUserToChannel(channel, user); } else await this.chatService.addUserToChannel(channel, user)
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
{ {
const conUser = new ConnectedUser(); const conUser = new ConnectedUser()
conUser.user = user; conUser.user = user
conUser.channel = channel; conUser.channel = channel
conUser.socket = socket.id; conUser.socket = socket.id
await this.connectedUserRepository.save(conUser); await this.connectedUserRepository.save(conUser)
} }
const messages = await this.messageService.findMessagesInChannelForUser( const messages = await this.messageService.findMessagesInChannelForUser(
channel, channel,
user user
); )
this.server.to(socket.id).emit("messages", messages); this.server.to(socket.id).emit('messages', messages)
await socket.join(channel.name); await socket.join(channel.name)
} }
@SubscribeMessage("leaveChannel") @SubscribeMessage('leaveChannel')
async onLeaveChannel(socket: Socket): Promise<void> { async onLeaveChannel (socket: Socket): Promise<void> {
const id = socket.id as any; const id = socket.id as any
await this.connectedUserRepository.delete({ socket: id }); await this.connectedUserRepository.delete({ socket: id })
socket.disconnect(); socket.disconnect()
} }
@SubscribeMessage("addMessage") @SubscribeMessage('addMessage')
async onAddMessage(socket: Socket, message: CreateMessageDto): Promise<void> { async onAddMessage (socket: Socket, message: CreateMessageDto): Promise<void> {
const channel = await this.chatService.getChannel(message.ChannelId); const channel = await this.chatService.getChannel(message.ChannelId)
if ( if (
(await this.chatService.getMuteDuration(channel.id, message.UserId)) > 0 (await this.chatService.getMuteDuration(channel.id, message.UserId)) > 0
) { ) {
throw new WsException("You are muted"); throw new WsException('You are muted')
} }
const createdMessage: Message = await this.messageService.createMessage( const createdMessage: Message = await this.messageService.createMessage(
message message
); )
socket.in(channel.name).emit("newMessage", createdMessage); socket.in(channel.name).emit('newMessage', createdMessage)
} }
@SubscribeMessage("kickUser") @SubscribeMessage('kickUser')
async onKickUser( async onKickUser (
socket: Socket, socket: Socket,
chan: number, chan: number,
from: number, from: number,
to: number to: number
): Promise<void> { ): Promise<void> {
const channel = await this.chatService.getChannel(chan); const channel = await this.chatService.getChannel(chan)
if ( if (
channel.owner.id !== from || channel.owner.id !== from ||
channel.admins.find((e) => e.id === from) == null channel.admins.find((e) => e.id === from) == null
) { ) {
throw new WsException("You do not have the required privileges"); throw new WsException('You do not have the required privileges')
} }
await this.onLeaveChannel(socket); await this.onLeaveChannel(socket)
} }
} }

26
back/volume/src/chat/chat.module.ts

@ -1,25 +1,25 @@
import { forwardRef, Module } from "@nestjs/common"; import { forwardRef, Module } from '@nestjs/common'
import { TypeOrmModule } from "@nestjs/typeorm"; import { TypeOrmModule } from '@nestjs/typeorm'
import { AuthModule } from "src/auth/auth.module"; import { AuthModule } from 'src/auth/auth.module'
import { UsersModule } from "src/users/users.module"; import { UsersModule } from 'src/users/users.module'
import { ChatGateway } from "./chat.gateway"; import { ChatGateway } from './chat.gateway'
import { ChatController } from "./chat.controller"; import { ChatController } from './chat.controller'
import { ChatService } from "./chat.service"; import { ChatService } from './chat.service'
import { MessageService } from "./message.service"; import { MessageService } from './message.service'
import Channel from "./entity/channel.entity"; import Channel from './entity/channel.entity'
import Message from "./entity/message.entity"; import Message from './entity/message.entity'
import ConnectedUser from "./entity/connection.entity"; import ConnectedUser from './entity/connection.entity'
@Module({ @Module({
imports: [ imports: [
UsersModule, UsersModule,
AuthModule, AuthModule,
TypeOrmModule.forFeature([Channel, Message, ConnectedUser]), TypeOrmModule.forFeature([Channel, Message, ConnectedUser])
], ],
controllers: [ChatController], controllers: [ChatController],
providers: [ChatService, ChatGateway, MessageService], providers: [ChatService, ChatGateway, MessageService],
exports: [ChatService], exports: [ChatService]
}) })
export class ChatModule {} export class ChatModule {}

186
back/volume/src/chat/chat.service.ts

@ -1,173 +1,173 @@
import { Inject, Injectable, NotFoundException } from "@nestjs/common"; import { Inject, Injectable, NotFoundException } from '@nestjs/common'
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from "typeorm"; import { Repository } from 'typeorm'
import { type CreateChannelDto } from "./dto/create-channel.dto"; import { type CreateChannelDto } from './dto/create-channel.dto'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
import type User from "src/users/entity/user.entity"; import type User from 'src/users/entity/user.entity'
import Channel from "./entity/channel.entity"; import Channel from './entity/channel.entity'
import { Cron } from "@nestjs/schedule"; import { Cron } from '@nestjs/schedule'
@Injectable() @Injectable()
export class ChatService { export class ChatService {
constructor( constructor (
@InjectRepository(Channel) @InjectRepository(Channel)
private readonly ChannelRepository: Repository<Channel>, private readonly ChannelRepository: Repository<Channel>,
private readonly usersService: UsersService private readonly usersService: UsersService
) {} ) {}
async createChannel(channel: CreateChannelDto): Promise<Channel> { async createChannel (channel: CreateChannelDto): Promise<Channel> {
const user: User | null = await this.usersService.findUser(channel.owner); const user: User | null = await this.usersService.findUser(channel.owner)
if (user == null) { if (user == null) {
throw new NotFoundException(`User #${channel.owner} not found`); throw new NotFoundException(`User #${channel.owner} not found`)
} }
const newChannel = new Channel(); const newChannel = new Channel()
newChannel.owner = user; newChannel.owner = user
newChannel.users = [user]; newChannel.users = [user]
newChannel.admins = [user]; newChannel.admins = [user]
newChannel.name = channel.name; newChannel.name = channel.name
newChannel.isPrivate = channel.isPrivate; newChannel.isPrivate = channel.isPrivate
newChannel.password = channel.password; newChannel.password = channel.password
return await this.ChannelRepository.save(newChannel); return await this.ChannelRepository.save(newChannel)
} }
async updatePassword(id: number, password: string) { async updatePassword (id: number, password: string) {
const channel: Channel | null = await this.ChannelRepository.findOneBy({ const channel: Channel | null = await this.ChannelRepository.findOneBy({
id, id
}); })
if (channel === null) { if (channel === null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
channel.password = password; channel.password = password
await this.ChannelRepository.save(channel); await this.ChannelRepository.save(channel)
} }
async getChannelsForUser(ftId: number): Promise<Channel[]> { async getChannelsForUser (ftId: number): Promise<Channel[]> {
let rooms: Channel[] = []; let rooms: Channel[] = []
rooms = [ rooms = [
...(await this.ChannelRepository.createQueryBuilder("room") ...(await this.ChannelRepository.createQueryBuilder('room')
.where("room.isPrivate = false") .where('room.isPrivate = false')
.getMany()), .getMany())
]; ]
rooms = [ rooms = [
...rooms, ...rooms,
...(await this.ChannelRepository.createQueryBuilder("room") ...(await this.ChannelRepository.createQueryBuilder('room')
.innerJoin("room.users", "users") .innerJoin('room.users', 'users')
.where("room.isPrivate = true") .where('room.isPrivate = true')
.andWhere("users.ftId = :ftId", { ftId }) .andWhere('users.ftId = :ftId', { ftId })
.getMany()), .getMany())
]; ]
return rooms; return rooms
} }
@Cron("*/6 * * * * *") @Cron('*/6 * * * * *')
async updateMutelists(): Promise<void> { async updateMutelists (): Promise<void> {
const channels = await this.ChannelRepository.find({}); const channels = await this.ChannelRepository.find({})
channels.forEach((channel) => { channels.forEach((channel) => {
channel.muted = channel.muted.filter((data) => { channel.muted = channel.muted.filter((data) => {
return data[0] - Date.now() > 0; return data[0] - Date.now() > 0
}); })
this.ChannelRepository.save(channel); this.ChannelRepository.save(channel)
}); })
} }
async addUserToChannel(channel: Channel, user: User): Promise<Channel> { async addUserToChannel (channel: Channel, user: User): Promise<Channel> {
channel.owner = user; channel.owner = user
return await this.ChannelRepository.save(channel); return await this.ChannelRepository.save(channel)
} }
async getChannel(id: number): Promise<Channel> { async getChannel (id: number): Promise<Channel> {
const channel = await this.ChannelRepository.findOneBy({ id }); const channel = await this.ChannelRepository.findOneBy({ id })
if (channel == null) { if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
return channel; return channel
} }
async getFullChannel(id: number): Promise<Channel> { async getFullChannel (id: number): Promise<Channel> {
const channel = await this.ChannelRepository.findOne({ const channel = await this.ChannelRepository.findOne({
where: { id }, where: { id },
relations: ["users", "admins", "banned", "muted", "owner"], relations: ['users', 'admins', 'banned', 'muted', 'owner']
}); })
if (channel == null) { if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
return channel; return channel
} }
async update(channel: Channel) { async update (channel: Channel) {
await this.ChannelRepository.update(channel.id, channel); await this.ChannelRepository.update(channel.id, channel)
} }
async save(channel: Channel) { async save (channel: Channel) {
await this.ChannelRepository.save(channel); await this.ChannelRepository.save(channel)
} }
async removeChannel(channelId: number) { async removeChannel (channelId: number) {
await this.ChannelRepository.delete(channelId); await this.ChannelRepository.delete(channelId)
} }
async isOwner(id: number, userId: number): Promise<boolean> { async isOwner (id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({ const channel = await this.ChannelRepository.findOne({
where: { id }, where: { id },
relations: { owner: true }, relations: { owner: true }
}); })
if (channel == null) { if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
return channel.owner.ftId === userId; return channel.owner.ftId === userId
} }
async isAdmin(id: number, userId: number): Promise<boolean> { async isAdmin (id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({ const channel = await this.ChannelRepository.findOne({
where: { id }, where: { id },
relations: { admins: true }, relations: { admins: true }
}); })
if (channel == null) { if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
return channel.admins.findIndex((user) => user.ftId === userId) != -1; return channel.admins.findIndex((user) => user.ftId === userId) != -1
} }
async isUser(id: number, userId: number): Promise<boolean> { async isUser (id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({ const channel = await this.ChannelRepository.findOne({
where: { id }, where: { id },
relations: { users: true }, relations: { users: true }
}); })
if (channel == null) { if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
return channel.users.findIndex((user) => user.ftId === userId) != -1; return channel.users.findIndex((user) => user.ftId === userId) != -1
} }
async isBanned(id: number, userId: number): Promise<boolean> { async isBanned (id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({ const channel = await this.ChannelRepository.findOne({
where: { id }, where: { id },
relations: { banned: true }, relations: { banned: true }
}); })
if (channel == null) { if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
return channel.banned.findIndex((user) => user.ftId === userId) != -1; return channel.banned.findIndex((user) => user.ftId === userId) != -1
} }
async getMuteDuration(id: number, userId: number): Promise<number> { async getMuteDuration (id: number, userId: number): Promise<number> {
const channel = await this.ChannelRepository.findOne({ const channel = await this.ChannelRepository.findOne({
where: { id }, where: { id },
relations: { muted: true }, relations: { muted: true }
}); })
if (channel == null) { if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`); throw new NotFoundException(`Channel #${id} not found`)
} }
const mutation: number[] | undefined = channel.muted.find( const mutation: number[] | undefined = channel.muted.find(
(mutation) => mutation[0] === userId (mutation) => mutation[0] === userId
); )
if (mutation == null) { if (mutation == null) {
return 0; return 0
} }
return mutation[1]; return mutation[1]
} }
} }

8
back/volume/src/chat/dto/connection.dto.ts

@ -1,13 +1,13 @@
import { IsNumber, IsOptional, IsString } from "class-validator"; import { IsNumber, IsOptional, IsString } from 'class-validator'
export class ConnectionDto { export class ConnectionDto {
@IsNumber() @IsNumber()
UserId: number; UserId: number
@IsNumber() @IsNumber()
ChannelId: number; ChannelId: number
@IsString() @IsString()
@IsOptional() @IsOptional()
pwd: string; pwd: string
} }

18
back/volume/src/chat/dto/create-channel.dto.ts

@ -1,28 +1,28 @@
import { Transform } from "class-transformer"; import { Transform } from 'class-transformer'
import { import {
IsPositive, IsPositive,
IsAlpha, IsAlpha,
IsString, IsString,
IsOptional, IsOptional,
IsNumber, IsNumber,
IsBoolean, IsBoolean
} from "class-validator"; } from 'class-validator'
export class CreateChannelDto { export class CreateChannelDto {
@IsOptional() @IsOptional()
@IsPositive() @IsPositive()
id: number; id: number
@IsString() @IsString()
name: string; name: string
@IsNumber() @IsNumber()
owner: number; owner: number
@IsOptional() @IsOptional()
password: string; password: string
@IsBoolean() @IsBoolean()
@Transform(({ value }) => value === "true") @Transform(({ value }) => value === 'true')
isPrivate: boolean; isPrivate: boolean
} }

8
back/volume/src/chat/dto/create-message.dto.ts

@ -1,12 +1,12 @@
import { IsNumber, IsString } from "class-validator"; import { IsNumber, IsString } from 'class-validator'
export class CreateMessageDto { export class CreateMessageDto {
@IsString() @IsString()
text: string; text: string
@IsNumber() @IsNumber()
UserId: number; UserId: number
@IsNumber() @IsNumber()
ChannelId: number; ChannelId: number
} }

20
back/volume/src/chat/dto/update-channel.dto.ts

@ -1,30 +1,30 @@
import { PartialType } from "@nestjs/mapped-types"; import { PartialType } from '@nestjs/mapped-types'
import { CreateChannelDto } from "./create-channel.dto"; import { CreateChannelDto } from './create-channel.dto'
import { IsNumber, IsOptional, IsString } from "class-validator"; import { IsNumber, IsOptional, IsString } from 'class-validator'
export class UpdateChannelDto extends PartialType(CreateChannelDto) { export class UpdateChannelDto extends PartialType(CreateChannelDto) {
id: number; id: number
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
users: [number]; users: [number]
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
messages: [number]; messages: [number]
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
owners: [number]; // user id owners: [number] // user id
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
banned: [number]; // user id banned: [number] // user id
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
muted: [number]; // user id muted: [number] // user id
@IsString() @IsString()
@IsOptional() @IsOptional()
password: string; password: string
} }

8
back/volume/src/chat/dto/updateUser.dto.ts

@ -1,15 +1,15 @@
import { IsNumber, IsString } from "class-validator"; import { IsNumber, IsString } from 'class-validator'
export class IdDto { export class IdDto {
@IsNumber() @IsNumber()
id: number; id: number
} }
export class PasswordDto { export class PasswordDto {
@IsString() @IsString()
password: string; password: string
} }
export class MuteDto { export class MuteDto {
data: number[]; data: number[]
} }

40
back/volume/src/chat/entity/channel.entity.ts

@ -8,54 +8,54 @@ import {
ManyToOne, ManyToOne,
OneToMany, OneToMany,
OneToOne, OneToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn
} from "typeorm"; } from 'typeorm'
import User from "src/users/entity/user.entity"; import User from 'src/users/entity/user.entity'
import Message from "./message.entity"; import Message from './message.entity'
import * as bcrypt from "bcrypt"; import * as bcrypt from 'bcrypt'
@Entity() @Entity()
export default class Channel { export default class Channel {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number
@Column() @Column()
name: string; name: string
@Column({ default: false }) @Column({ default: false })
isPrivate: boolean; isPrivate: boolean
@Column({ select: false, default: "" }) @Column({ select: false, default: '' })
password: string; password: string
@BeforeInsert() @BeforeInsert()
async hashPassword() { async hashPassword () {
if (this.password === "") return; if (this.password === '') return
this.password = await bcrypt.hash( this.password = await bcrypt.hash(
this.password, this.password,
Number(process.env.HASH_SALT) Number(process.env.HASH_SALT)
); )
} }
@ManyToMany(() => User) @ManyToMany(() => User)
@JoinTable() @JoinTable()
users: User[]; users: User[]
@OneToMany(() => Message, (message: Message) => message.channel) @OneToMany(() => Message, (message: Message) => message.channel)
messages: Message[]; messages: Message[]
@ManyToOne(() => User) @ManyToOne(() => User)
@JoinColumn() @JoinColumn()
owner: User; owner: User
@ManyToMany(() => User) @ManyToMany(() => User)
@JoinTable() @JoinTable()
admins: User[]; admins: User[]
@ManyToMany(() => User) // refuse connection @ManyToMany(() => User) // refuse connection
@JoinTable() @JoinTable()
banned: User[]; banned: User[]
@Column("text", { array: true }) @Column('text', { array: true })
muted: number[][]; muted: number[][]
} }

14
back/volume/src/chat/entity/connection.entity.ts

@ -3,21 +3,21 @@ import {
Entity, Entity,
JoinColumn, JoinColumn,
OneToOne, OneToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn
} from "typeorm"; } from 'typeorm'
import Channel from "./channel.entity"; import Channel from './channel.entity'
import User from "src/users/entity/user.entity"; import User from 'src/users/entity/user.entity'
@Entity() @Entity()
export default class ConnectedUser { export default class ConnectedUser {
@OneToOne(() => User) @OneToOne(() => User)
user: User; user: User
@OneToOne(() => Channel) @OneToOne(() => Channel)
@JoinColumn() @JoinColumn()
channel: Channel; channel: Channel
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
socket: string; socket: string
} }

12
back/volume/src/chat/entity/dm.entity.ts

@ -1,15 +1,15 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'
import Message from "./message.entity"; import Message from './message.entity'
import type User from "src/users/entity/user.entity"; import type User from 'src/users/entity/user.entity'
@Entity() @Entity()
export class Channel { export class Channel {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number
@Column() @Column()
users: User[]; users: User[]
@OneToMany(() => Message, (message) => message.channel) @OneToMany(() => Message, (message) => message.channel)
messages: Message[]; messages: Message[]
} }

18
back/volume/src/chat/entity/message.entity.ts

@ -5,27 +5,27 @@ import {
JoinColumn, JoinColumn,
JoinTable, JoinTable,
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn
} from "typeorm"; } from 'typeorm'
import User from "src/users/entity/user.entity"; import User from 'src/users/entity/user.entity'
import Channel from "./channel.entity"; import Channel from './channel.entity'
@Entity() @Entity()
export default class Message { export default class Message {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number
@Column() @Column()
text: string; text: string
@ManyToOne(() => User) @ManyToOne(() => User)
@JoinColumn() @JoinColumn()
author: User; author: User
@ManyToOne(() => Channel, (channel) => channel.messages, { cascade: true }) @ManyToOne(() => Channel, (channel) => channel.messages, { cascade: true })
@JoinTable() @JoinTable()
channel: Channel; channel: Channel
@CreateDateColumn() @CreateDateColumn()
created_at: Date; created_at: Date
} }

46
back/volume/src/chat/message.service.ts

@ -1,43 +1,43 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common'
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from "typeorm"; import { Repository } from 'typeorm'
import { ChatService } from "./chat.service"; import { ChatService } from './chat.service'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
import { type CreateMessageDto } from "./dto/create-message.dto"; import { type CreateMessageDto } from './dto/create-message.dto'
import type User from "src/users/entity/user.entity"; import type User from 'src/users/entity/user.entity'
import type Channel from "./entity/channel.entity"; import type Channel from './entity/channel.entity'
import Message from "./entity/message.entity"; import Message from './entity/message.entity'
@Injectable() @Injectable()
export class MessageService { export class MessageService {
constructor( constructor (
@InjectRepository(Message) @InjectRepository(Message)
private readonly MessageRepository: Repository<Message>, private readonly MessageRepository: Repository<Message>,
private readonly channelService: ChatService, private readonly channelService: ChatService,
private readonly usersService: UsersService private readonly usersService: UsersService
) {} ) {}
async createMessage(message: CreateMessageDto): Promise<Message> { async createMessage (message: CreateMessageDto): Promise<Message> {
const msg = new Message(); const msg = new Message()
msg.text = message.text; msg.text = message.text
msg.channel = await this.channelService.getChannel(message.ChannelId); msg.channel = await this.channelService.getChannel(message.ChannelId)
msg.author = (await this.usersService.findUser(message.UserId)) as User; msg.author = (await this.usersService.findUser(message.UserId)) as User
msg.channel.messages.push(msg); msg.channel.messages.push(msg)
return await this.MessageRepository.save( return await this.MessageRepository.save(
this.MessageRepository.create(msg) this.MessageRepository.create(msg)
); )
} }
async findMessagesInChannelForUser( async findMessagesInChannelForUser (
channel: Channel, channel: Channel,
user: User user: User
): Promise<Message[]> { ): Promise<Message[]> {
return await this.MessageRepository.createQueryBuilder("message") return await this.MessageRepository.createQueryBuilder('message')
.where("message.channel = :chan", { chan: channel }) .where('message.channel = :chan', { chan: channel })
.andWhere("message.author NOT IN (:...blocked)", { .andWhere('message.author NOT IN (:...blocked)', {
blocked: user.blocked, blocked: user.blocked
}) })
.getMany(); .getMany()
} }
} }

56
back/volume/src/main.ts

@ -1,45 +1,45 @@
import { InternalServerErrorException, Logger } from "@nestjs/common"; import { InternalServerErrorException, Logger } from '@nestjs/common'
import { NestFactory } from "@nestjs/core"; import { NestFactory } from '@nestjs/core'
import { AppModule } from "./app.module"; import { AppModule } from './app.module'
import * as session from "express-session"; import * as session from 'express-session'
import * as passport from "passport"; import * as passport from 'passport'
import { type NestExpressApplication } from "@nestjs/platform-express"; import { type NestExpressApplication } from '@nestjs/platform-express'
import * as cookieParser from "cookie-parser"; import * as cookieParser from 'cookie-parser'
import { IoAdapter } from "@nestjs/platform-socket.io"; import { IoAdapter } from '@nestjs/platform-socket.io'
async function bootstrap(): Promise<void> { async function bootstrap (): Promise<void> {
const logger = new Logger(); const logger = new Logger()
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule)
const port = const port =
process.env.BACK_PORT !== undefined && process.env.BACK_PORT !== "" process.env.BACK_PORT !== undefined && process.env.BACK_PORT !== ''
? +process.env.BACK_PORT ? +process.env.BACK_PORT
: 3001; : 3001
const cors = { const cors = {
origin: /^(http|ws):\/\/localhost(:\d+)?$/, origin: /^(http|ws):\/\/localhost(:\d+)?$/,
methods: "GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS", methods: 'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS',
preflightContinue: false, preflightContinue: false,
optionsSuccessStatus: 204, optionsSuccessStatus: 204,
credentials: true, credentials: true,
allowedHeaders: ["Accept", "Content-Type", "Authorization"], allowedHeaders: ['Accept', 'Content-Type', 'Authorization']
}; }
app.use( app.use(
session({ session({
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
secret: secret:
process.env.JWT_SECRET !== undefined && process.env.JWT_SECRET !== "" process.env.JWT_SECRET !== undefined && process.env.JWT_SECRET !== ''
? process.env.JWT_SECRET ? process.env.JWT_SECRET
: "secret", : 'secret'
}) })
); )
app.use(cookieParser()); app.use(cookieParser())
app.use(passport.initialize()); app.use(passport.initialize())
app.use(passport.session()); app.use(passport.session())
app.enableCors(cors); app.enableCors(cors)
app.useWebSocketAdapter(new IoAdapter(app)); app.useWebSocketAdapter(new IoAdapter(app))
await app.listen(port); await app.listen(port)
logger.log(`Application listening on port ${port}`); logger.log(`Application listening on port ${port}`)
} }
bootstrap().catch((e) => { bootstrap().catch((e) => {
throw new InternalServerErrorException(e); throw new InternalServerErrorException(e)
}); })

20
back/volume/src/pong/dtos/GameCreationDtoValidated.ts

@ -1,4 +1,4 @@
import { Type } from "class-transformer"; import { Type } from 'class-transformer'
import { import {
ArrayMaxSize, ArrayMaxSize,
ArrayMinSize, ArrayMinSize,
@ -7,32 +7,32 @@ import {
IsString, IsString,
Max, Max,
Min, Min,
ValidateNested, ValidateNested
} from "class-validator"; } from 'class-validator'
import { import {
DEFAULT_BALL_INITIAL_SPEED, DEFAULT_BALL_INITIAL_SPEED,
DEFAULT_MAX_BALL_SPEED, DEFAULT_MAX_BALL_SPEED
} from "../game/constants"; } from '../game/constants'
import { MapDtoValidated } from "./MapDtoValidated"; import { MapDtoValidated } from './MapDtoValidated'
export class GameCreationDtoValidated { export class GameCreationDtoValidated {
@IsString({ each: true }) @IsString({ each: true })
@ArrayMaxSize(2) @ArrayMaxSize(2)
@ArrayMinSize(2) @ArrayMinSize(2)
playerNames!: string[]; playerNames!: string[]
@IsNotEmptyObject() @IsNotEmptyObject()
@ValidateNested() @ValidateNested()
@Type(() => MapDtoValidated) @Type(() => MapDtoValidated)
map!: MapDtoValidated; map!: MapDtoValidated
@IsNumber() @IsNumber()
@Min(DEFAULT_BALL_INITIAL_SPEED.x) @Min(DEFAULT_BALL_INITIAL_SPEED.x)
@Max(DEFAULT_MAX_BALL_SPEED.x) @Max(DEFAULT_MAX_BALL_SPEED.x)
initialBallSpeedX!: number; initialBallSpeedX!: number
@IsNumber() @IsNumber()
@Min(DEFAULT_BALL_INITIAL_SPEED.y) @Min(DEFAULT_BALL_INITIAL_SPEED.y)
@Max(DEFAULT_MAX_BALL_SPEED.y) @Max(DEFAULT_MAX_BALL_SPEED.y)
initialBallSpeedY!: number; initialBallSpeedY!: number
} }

18
back/volume/src/pong/dtos/GameInfo.ts

@ -1,12 +1,12 @@
import { type Point, type Rect } from "../game/utils"; import { type Point, type Rect } from '../game/utils'
export class GameInfo { export class GameInfo {
mapSize!: Point; mapSize!: Point
yourPaddleIndex!: number; yourPaddleIndex!: number
gameId!: string; gameId!: string
walls!: Rect[]; walls!: Rect[]
paddleSize!: Point; paddleSize!: Point
ballSize!: Point; ballSize!: Point
winScore!: number; winScore!: number
ranked!: boolean; ranked!: boolean
} }

10
back/volume/src/pong/dtos/GameUpdate.ts

@ -1,8 +1,8 @@
import { type Point } from "../game/utils"; import { type Point } from '../game/utils'
export class GameUpdate { export class GameUpdate {
paddlesPositions!: Point[]; paddlesPositions!: Point[]
ballSpeed!: Point; ballSpeed!: Point
ballPosition!: Point; ballPosition!: Point
scores!: number[]; scores!: number[]
} }

14
back/volume/src/pong/dtos/MapDtoValidated.ts

@ -1,23 +1,23 @@
import { Type } from "class-transformer"; import { Type } from 'class-transformer'
import { import {
ArrayMaxSize, ArrayMaxSize,
IsArray, IsArray,
IsDefined, IsDefined,
IsObject, IsObject,
ValidateNested, ValidateNested
} from "class-validator"; } from 'class-validator'
import { PointDtoValidated } from "./PointDtoValidated"; import { PointDtoValidated } from './PointDtoValidated'
import { RectDtoValidated } from "./RectDtoValidated"; import { RectDtoValidated } from './RectDtoValidated'
export class MapDtoValidated { export class MapDtoValidated {
@IsObject() @IsObject()
@IsDefined() @IsDefined()
@Type(() => PointDtoValidated) @Type(() => PointDtoValidated)
size!: PointDtoValidated; size!: PointDtoValidated
@IsArray() @IsArray()
@ArrayMaxSize(5) @ArrayMaxSize(5)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => RectDtoValidated) @Type(() => RectDtoValidated)
walls!: RectDtoValidated[]; walls!: RectDtoValidated[]
} }

2
back/volume/src/pong/dtos/MatchmakingDto.ts

@ -1,3 +1,3 @@
export class MatchmakingDto { export class MatchmakingDto {
matchmaking!: boolean; matchmaking!: boolean
} }

6
back/volume/src/pong/dtos/MatchmakingDtoValidated.ts

@ -1,7 +1,7 @@
import { IsBoolean } from "class-validator"; import { IsBoolean } from 'class-validator'
import { MatchmakingDto } from "./MatchmakingDto"; import { MatchmakingDto } from './MatchmakingDto'
export class MatchmakingDtoValidated extends MatchmakingDto { export class MatchmakingDtoValidated extends MatchmakingDto {
@IsBoolean() @IsBoolean()
matchmaking!: boolean; matchmaking!: boolean
} }

8
back/volume/src/pong/dtos/PointDtoValidated.ts

@ -1,10 +1,10 @@
import { IsNumber } from "class-validator"; import { IsNumber } from 'class-validator'
import { Point } from "../game/utils"; import { Point } from '../game/utils'
export class PointDtoValidated extends Point { export class PointDtoValidated extends Point {
@IsNumber() @IsNumber()
x!: number; x!: number
@IsNumber() @IsNumber()
y!: number; y!: number
} }

12
back/volume/src/pong/dtos/RectDtoValidated.ts

@ -1,14 +1,14 @@
import { Type } from "class-transformer"; import { Type } from 'class-transformer'
import { ValidateNested } from "class-validator"; import { ValidateNested } from 'class-validator'
import { Rect } from "../game/utils"; import { Rect } from '../game/utils'
import { PointDtoValidated } from "./PointDtoValidated"; import { PointDtoValidated } from './PointDtoValidated'
export class RectDtoValidated extends Rect { export class RectDtoValidated extends Rect {
@ValidateNested() @ValidateNested()
@Type(() => PointDtoValidated) @Type(() => PointDtoValidated)
center!: PointDtoValidated; center!: PointDtoValidated
@ValidateNested() @ValidateNested()
@Type(() => PointDtoValidated) @Type(() => PointDtoValidated)
size!: PointDtoValidated; size!: PointDtoValidated
} }

2
back/volume/src/pong/dtos/StringDto.ts

@ -1,3 +1,3 @@
export class StringDto { export class StringDto {
value!: string; value!: string
} }

6
back/volume/src/pong/dtos/StringDtoValidated.ts

@ -1,7 +1,7 @@
import { IsString } from "class-validator"; import { IsString } from 'class-validator'
import { StringDto } from "./StringDto"; import { StringDto } from './StringDto'
export class StringDtoValidated extends StringDto { export class StringDtoValidated extends StringDto {
@IsString() @IsString()
value!: string; value!: string
} }

8
back/volume/src/pong/dtos/UserDto.ts

@ -1,12 +1,12 @@
import { IsString } from "class-validator"; import { IsString } from 'class-validator'
export class UserDto { export class UserDto {
@IsString() @IsString()
username!: string; username!: string
@IsString() @IsString()
avatar!: string; avatar!: string
@IsString() @IsString()
status!: string; status!: string
} }

18
back/volume/src/pong/entity/result.entity.ts

@ -3,25 +3,25 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Column, Column,
ManyToMany, ManyToMany,
CreateDateColumn, CreateDateColumn
} from "typeorm"; } from 'typeorm'
import User from "src/users/entity/user.entity"; import User from 'src/users/entity/user.entity'
@Entity() @Entity()
export default class Result { export default class Result {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number
@Column({ default: false }) @Column({ default: false })
ranked: boolean; ranked: boolean
@ManyToMany(() => User, (player: User) => player.results, { cascade: true }) @ManyToMany(() => User, (player: User) => player.results, { cascade: true })
players: Array<User | null>; // TODO: change to User[] for final version players: Array<User | null> // TODO: change to User[] for final version
@Column("text", { array: true }) @Column('text', { array: true })
public score: number[]; public score: number[]
@CreateDateColumn() @CreateDateColumn()
date: Date; date: Date
} }

106
back/volume/src/pong/game/Ball.ts

@ -1,114 +1,114 @@
import { type Paddle } from "./Paddle"; import { type Paddle } from './Paddle'
import { type Point, Rect } from "./utils"; import { type Point, Rect } from './utils'
import { type MapDtoValidated } from "../dtos/MapDtoValidated"; import { type MapDtoValidated } from '../dtos/MapDtoValidated'
import { import {
DEFAULT_BALL_SIZE, DEFAULT_BALL_SIZE,
GAME_TICKS, GAME_TICKS,
DEFAULT_BALL_SPEED_INCREMENT, DEFAULT_BALL_SPEED_INCREMENT,
DEFAULT_MAX_BALL_SPEED, DEFAULT_MAX_BALL_SPEED
} from "./constants"; } from './constants'
export class Ball { export class Ball {
rect: Rect; rect: Rect
initial_speed: Point; initial_speed: Point
speed: Point; speed: Point
spawn: Point; spawn: Point
indexPlayerScored: number; indexPlayerScored: number
timeoutTime: number; timeoutTime: number
constructor( constructor (
spawn: Point, spawn: Point,
initialSpeed: Point, initialSpeed: Point,
size: Point = DEFAULT_BALL_SIZE.clone() size: Point = DEFAULT_BALL_SIZE.clone()
) { ) {
this.rect = new Rect(spawn, size); this.rect = new Rect(spawn, size)
this.speed = initialSpeed.clone(); this.speed = initialSpeed.clone()
this.initial_speed = initialSpeed.clone(); this.initial_speed = initialSpeed.clone()
this.spawn = spawn.clone(); this.spawn = spawn.clone()
this.indexPlayerScored = -1; this.indexPlayerScored = -1
this.timeoutTime = 0; this.timeoutTime = 0
} }
getIndexPlayerScored(): number { getIndexPlayerScored (): number {
return this.indexPlayerScored; return this.indexPlayerScored
} }
update(canvasRect: Rect, paddles: Paddle[], map: MapDtoValidated): void { update (canvasRect: Rect, paddles: Paddle[], map: MapDtoValidated): void {
if (!canvasRect.contains_x(this.rect)) { if (!canvasRect.contains_x(this.rect)) {
this.indexPlayerScored = this.playerScored(); this.indexPlayerScored = this.playerScored()
this.timeoutTime = 2000; this.timeoutTime = 2000
} else { } else {
this.indexPlayerScored = -1; this.indexPlayerScored = -1
if (this.timeoutTime <= 0) { if (this.timeoutTime <= 0) {
this.move(canvasRect, paddles, map); this.move(canvasRect, paddles, map)
} else { } else {
this.timeoutTime -= 1000 / GAME_TICKS; this.timeoutTime -= 1000 / GAME_TICKS
} }
} }
} }
move(canvasRect: Rect, paddles: Paddle[], map: MapDtoValidated): void { move (canvasRect: Rect, paddles: Paddle[], map: MapDtoValidated): void {
for (const paddle of paddles) { for (const paddle of paddles) {
if (paddle.rect.collides(this.rect)) { if (paddle.rect.collides(this.rect)) {
if (this.speed.x < 0) { if (this.speed.x < 0) {
this.rect.center.x = paddle.rect.center.x + paddle.rect.size.x; this.rect.center.x = paddle.rect.center.x + paddle.rect.size.x
} else this.rect.center.x = paddle.rect.center.x - paddle.rect.size.x; } else this.rect.center.x = paddle.rect.center.x - paddle.rect.size.x
this.speed.x = this.speed.x * -1; this.speed.x = this.speed.x * -1
this.speed.y = this.speed.y =
((this.rect.center.y - paddle.rect.center.y) / paddle.rect.size.y) * ((this.rect.center.y - paddle.rect.center.y) / paddle.rect.size.y) *
20; 20
break; break
} }
} }
for (const wall of map.walls) { for (const wall of map.walls) {
if (wall.collides(this.rect)) { if (wall.collides(this.rect)) {
if (this.speed.x < 0) { if (this.speed.x < 0) {
this.rect.center.x = wall.center.x + wall.size.x; this.rect.center.x = wall.center.x + wall.size.x
} else this.rect.center.x = wall.center.x - wall.size.x; } else this.rect.center.x = wall.center.x - wall.size.x
this.speed.x = this.speed.x * -1; this.speed.x = this.speed.x * -1
this.speed.y = this.speed.y =
((this.rect.center.y - wall.center.y) / wall.size.y) * 20; ((this.rect.center.y - wall.center.y) / wall.size.y) * 20
break; break
} }
} }
if (!canvasRect.contains_y(this.rect)) this.speed.y = this.speed.y * -1; if (!canvasRect.contains_y(this.rect)) this.speed.y = this.speed.y * -1
if (this.speed.x > 0 && this.speed.x < DEFAULT_MAX_BALL_SPEED.x) { if (this.speed.x > 0 && this.speed.x < DEFAULT_MAX_BALL_SPEED.x) {
this.speed.x += DEFAULT_BALL_SPEED_INCREMENT.x; this.speed.x += DEFAULT_BALL_SPEED_INCREMENT.x
} }
if (this.speed.x < 0 && this.speed.x > -DEFAULT_MAX_BALL_SPEED.x) { if (this.speed.x < 0 && this.speed.x > -DEFAULT_MAX_BALL_SPEED.x) {
this.speed.x -= DEFAULT_BALL_SPEED_INCREMENT.x; this.speed.x -= DEFAULT_BALL_SPEED_INCREMENT.x
} }
if (this.speed.y > 0 && this.speed.y > DEFAULT_MAX_BALL_SPEED.y) { if (this.speed.y > 0 && this.speed.y > DEFAULT_MAX_BALL_SPEED.y) {
this.speed.y += DEFAULT_MAX_BALL_SPEED.y; this.speed.y += DEFAULT_MAX_BALL_SPEED.y
} }
if (this.speed.y < 0 && this.speed.y < -DEFAULT_MAX_BALL_SPEED.y) { if (this.speed.y < 0 && this.speed.y < -DEFAULT_MAX_BALL_SPEED.y) {
this.speed.y -= DEFAULT_MAX_BALL_SPEED.y; this.speed.y -= DEFAULT_MAX_BALL_SPEED.y
} }
this.rect.center.add_inplace(this.speed); this.rect.center.add_inplace(this.speed)
} }
playerScored(): number { playerScored (): number {
let indexPlayerScored: number; let indexPlayerScored: number
if (this.rect.center.x <= this.spawn.x) { if (this.rect.center.x <= this.spawn.x) {
indexPlayerScored = 1; indexPlayerScored = 1
this.speed.x = this.initial_speed.x; this.speed.x = this.initial_speed.x
} else { } else {
indexPlayerScored = 0; indexPlayerScored = 0
this.speed.x = -this.initial_speed.x; this.speed.x = -this.initial_speed.x
} }
if (this.speed.y < 0) { if (this.speed.y < 0) {
this.speed.y = this.initial_speed.y; this.speed.y = this.initial_speed.y
} else { } else {
this.speed.y = -this.initial_speed.y; this.speed.y = -this.initial_speed.y
} }
this.rect.center = this.spawn.clone(); this.rect.center = this.spawn.clone()
return indexPlayerScored; return indexPlayerScored
} }
} }

158
back/volume/src/pong/game/Game.ts

@ -1,33 +1,33 @@
import { Ball } from "./Ball"; import { Ball } from './Ball'
import { type Socket } from "socket.io"; import { type Socket } from 'socket.io'
import { Point, Rect } from "./utils"; import { Point, Rect } from './utils'
import { Player } from "./Player"; import { Player } from './Player'
import { import {
DEFAULT_BALL_INITIAL_SPEED, DEFAULT_BALL_INITIAL_SPEED,
DEFAULT_BALL_SIZE, DEFAULT_BALL_SIZE,
DEFAULT_PADDLE_SIZE, DEFAULT_PADDLE_SIZE,
DEFAULT_WIN_SCORE, DEFAULT_WIN_SCORE,
GAME_EVENTS, GAME_EVENTS,
GAME_TICKS, GAME_TICKS
} from "./constants"; } from './constants'
import { randomUUID } from "crypto"; import { randomUUID } from 'crypto'
import { type MapDtoValidated } from "../dtos/MapDtoValidated"; import { type MapDtoValidated } from '../dtos/MapDtoValidated'
import { type GameUpdate } from "../dtos/GameUpdate"; import { type GameUpdate } from '../dtos/GameUpdate'
import { type GameInfo } from "../dtos/GameInfo"; import { type GameInfo } from '../dtos/GameInfo'
import { type PongService } from "../pong.service"; import { type PongService } from '../pong.service'
export class Game { export class Game {
id: string; id: string
timer: NodeJS.Timer | null; timer: NodeJS.Timer | null
map: MapDtoValidated; map: MapDtoValidated
ball: Ball; ball: Ball
players: Player[] = []; players: Player[] = []
ranked: boolean; ranked: boolean
initialBallSpeed: Point; initialBallSpeed: Point
waitingForTimeout: boolean; waitingForTimeout: boolean
gameStoppedCallback: (name: string) => void; gameStoppedCallback: (name: string) => void
constructor( constructor (
sockets: Socket[], sockets: Socket[],
uuids: string[], uuids: string[],
names: string[], names: string[],
@ -37,24 +37,24 @@ export class Game {
ranked: boolean, ranked: boolean,
initialBallSpeed: Point = DEFAULT_BALL_INITIAL_SPEED.clone() initialBallSpeed: Point = DEFAULT_BALL_INITIAL_SPEED.clone()
) { ) {
this.id = randomUUID(); this.id = randomUUID()
this.timer = null; this.timer = null
this.ranked = ranked; this.ranked = ranked
this.waitingForTimeout = false; this.waitingForTimeout = false
this.map = map; this.map = map
this.gameStoppedCallback = gameStoppedCallback; this.gameStoppedCallback = gameStoppedCallback
this.initialBallSpeed = initialBallSpeed; this.initialBallSpeed = initialBallSpeed
this.ball = new Ball( this.ball = new Ball(
new Point(this.map.size.x / 2, this.map.size.y / 2), new Point(this.map.size.x / 2, this.map.size.y / 2),
initialBallSpeed initialBallSpeed
); )
for (let i = 0; i < uuids.length; i++) { for (let i = 0; i < uuids.length; i++) {
this.addPlayer(sockets[i], uuids[i], names[i]); this.addPlayer(sockets[i], uuids[i], names[i])
} }
} }
getGameInfo(name: string): GameInfo { getGameInfo (name: string): GameInfo {
const yourPaddleIndex = this.players.findIndex((p) => p.name === name); const yourPaddleIndex = this.players.findIndex((p) => p.name === name)
return { return {
mapSize: this.map.size, mapSize: this.map.size,
yourPaddleIndex, yourPaddleIndex,
@ -63,112 +63,112 @@ export class Game {
paddleSize: DEFAULT_PADDLE_SIZE, paddleSize: DEFAULT_PADDLE_SIZE,
ballSize: DEFAULT_BALL_SIZE, ballSize: DEFAULT_BALL_SIZE,
winScore: DEFAULT_WIN_SCORE, winScore: DEFAULT_WIN_SCORE,
ranked: this.ranked, ranked: this.ranked
}; }
} }
private addPlayer(socket: Socket, uuid: string, name: string): void { private addPlayer (socket: Socket, uuid: string, name: string): void {
let paddleCoords = new Point( let paddleCoords = new Point(
DEFAULT_PADDLE_SIZE.x / 2, DEFAULT_PADDLE_SIZE.x / 2,
this.map.size.y / 2 this.map.size.y / 2
); )
if (this.players.length === 1) { if (this.players.length === 1) {
paddleCoords = new Point( paddleCoords = new Point(
this.map.size.x - DEFAULT_PADDLE_SIZE.x / 2, this.map.size.x - DEFAULT_PADDLE_SIZE.x / 2,
this.map.size.y / 2 this.map.size.y / 2
); )
} }
this.players.push( this.players.push(
new Player(socket, uuid, name, paddleCoords, this.map.size) new Player(socket, uuid, name, paddleCoords, this.map.size)
); )
if (this.ranked) { if (this.ranked) {
this.ready(name); this.ready(name)
} }
} }
ready(name: string): void { ready (name: string): void {
const playerIndex: number = this.players.findIndex((p) => p.name === name); const playerIndex: number = this.players.findIndex((p) => p.name === name)
if (playerIndex !== -1 && !this.players[playerIndex].ready) { if (playerIndex !== -1 && !this.players[playerIndex].ready) {
this.players[playerIndex].ready = true; this.players[playerIndex].ready = true
console.log(`${this.players[playerIndex].name} is ready`); console.log(`${this.players[playerIndex].name} is ready`)
if (this.players.length === 2 && this.players.every((p) => p.ready)) { if (this.players.length === 2 && this.players.every((p) => p.ready)) {
this.start(); this.start()
} }
} }
} }
private start(): void { private start (): void {
if (this.timer === null && this.players.length === 2) { if (this.timer === null && this.players.length === 2) {
this.ball = new Ball( this.ball = new Ball(
new Point(this.map.size.x / 2, this.map.size.y / 2), new Point(this.map.size.x / 2, this.map.size.y / 2),
this.initialBallSpeed this.initialBallSpeed
); )
this.players.forEach((p) => { this.players.forEach((p) => {
void this.pongService.setInGame(p.name); void this.pongService.setInGame(p.name)
p.newGame(); p.newGame()
}); })
this.broadcastGame(GAME_EVENTS.START_GAME); this.broadcastGame(GAME_EVENTS.START_GAME)
this.timer = setInterval(this.gameLoop.bind(this), 1000 / GAME_TICKS); this.timer = setInterval(this.gameLoop.bind(this), 1000 / GAME_TICKS)
console.log(`Game ${this.id} starting in 3 seconds`); console.log(`Game ${this.id} starting in 3 seconds`)
this.waitingForTimeout = true; this.waitingForTimeout = true
new Promise((resolve) => setTimeout(resolve, 3000)) new Promise((resolve) => setTimeout(resolve, 3000))
.then(() => (this.waitingForTimeout = false)) .then(() => (this.waitingForTimeout = false))
.catch(() => {}); .catch(() => {})
} }
} }
stop(): void { stop (): void {
if (this.timer !== null) { if (this.timer !== null) {
clearInterval(this.timer); clearInterval(this.timer)
} }
this.timer = null; this.timer = null
this.pongService this.pongService
.saveResult(this.players, this.ranked, DEFAULT_WIN_SCORE) .saveResult(this.players, this.ranked, DEFAULT_WIN_SCORE)
.then(() => { .then(() => {
this.gameStoppedCallback(this.players[0].name); this.gameStoppedCallback(this.players[0].name)
this.players = []; this.players = []
}) })
.catch(() => { .catch(() => {
this.gameStoppedCallback(this.players[0].name); this.gameStoppedCallback(this.players[0].name)
this.players = []; this.players = []
}); })
} }
movePaddle(name: string | undefined, position: Point): void { movePaddle (name: string | undefined, position: Point): void {
const playerIndex: number = this.players.findIndex((p) => p.name === name); const playerIndex: number = this.players.findIndex((p) => p.name === name)
if (this.timer !== null && playerIndex !== -1) { if (this.timer !== null && playerIndex !== -1) {
this.players[playerIndex].paddle.move(position.y); this.players[playerIndex].paddle.move(position.y)
} }
} }
private broadcastGame(event: string, data?: any): void { private broadcastGame (event: string, data?: any): void {
this.players.forEach((p) => { this.players.forEach((p) => {
p.socket.emit(event, data); p.socket.emit(event, data)
}); })
} }
private gameLoop(): void { private gameLoop (): void {
if (this.waitingForTimeout) { if (this.waitingForTimeout) {
return; return
} }
const canvasRect: Rect = new Rect( const canvasRect: Rect = new Rect(
new Point(this.map.size.x / 2, this.map.size.y / 2), new Point(this.map.size.x / 2, this.map.size.y / 2),
new Point(this.map.size.x, this.map.size.y) new Point(this.map.size.x, this.map.size.y)
); )
this.ball.update( this.ball.update(
canvasRect, canvasRect,
this.players.map((p) => p.paddle), this.players.map((p) => p.paddle),
this.map this.map
); )
const indexPlayerScored: number = this.ball.getIndexPlayerScored(); const indexPlayerScored: number = this.ball.getIndexPlayerScored()
if (indexPlayerScored !== -1) { if (indexPlayerScored !== -1) {
this.players[indexPlayerScored].score += 1; this.players[indexPlayerScored].score += 1
if (this.players[indexPlayerScored].score >= DEFAULT_WIN_SCORE) { if (this.players[indexPlayerScored].score >= DEFAULT_WIN_SCORE) {
console.log(`${this.players[indexPlayerScored].name} won`); console.log(`${this.players[indexPlayerScored].name} won`)
this.stop(); this.stop()
} }
} }
@ -176,8 +176,8 @@ export class Game {
paddlesPositions: this.players.map((p) => p.paddle.rect.center), paddlesPositions: this.players.map((p) => p.paddle.rect.center),
ballSpeed: this.ball.speed, ballSpeed: this.ball.speed,
ballPosition: this.ball.rect.center, ballPosition: this.ball.rect.center,
scores: this.players.map((p) => p.score), scores: this.players.map((p) => p.score)
}; }
this.broadcastGame(GAME_EVENTS.GAME_TICK, data); this.broadcastGame(GAME_EVENTS.GAME_TICK, data)
} }
} }

96
back/volume/src/pong/game/Games.ts

@ -1,33 +1,33 @@
import { type Socket } from "socket.io"; import { type Socket } from 'socket.io'
import { Game } from "./Game"; import { Game } from './Game'
import { Point } from "./utils"; import { Point } from './utils'
import { type MapDtoValidated as GameMap } from "../dtos/MapDtoValidated"; import { type MapDtoValidated as GameMap } from '../dtos/MapDtoValidated'
import { type GameCreationDtoValidated } from "../dtos/GameCreationDtoValidated"; import { type GameCreationDtoValidated } from '../dtos/GameCreationDtoValidated'
import { type GameInfo } from "../dtos/GameInfo"; import { type GameInfo } from '../dtos/GameInfo'
import { type PongService } from "../pong.service"; import { type PongService } from '../pong.service'
import { import {
DEFAULT_BALL_SIZE, DEFAULT_BALL_SIZE,
DEFAULT_MAP_SIZE, DEFAULT_MAP_SIZE,
DEFAULT_PADDLE_SIZE, DEFAULT_PADDLE_SIZE,
DEFAULT_WIN_SCORE, DEFAULT_WIN_SCORE
} from "./constants"; } from './constants'
export class Games { export class Games {
constructor(private readonly pongService: PongService) {} constructor (private readonly pongService: PongService) {}
private readonly playerNameToGameIndex = new Map<string, number>(); private readonly playerNameToGameIndex = new Map<string, number>()
private readonly games = new Array<Game>(); private readonly games = new Array<Game>()
newGame( newGame (
sockets: Socket[], sockets: Socket[],
uuids: string[], uuids: string[],
gameCreationDto: GameCreationDtoValidated, gameCreationDto: GameCreationDtoValidated,
ranked: boolean ranked: boolean
): void { ): void {
const names: string[] = gameCreationDto.playerNames; const names: string[] = gameCreationDto.playerNames
const map: GameMap = { const map: GameMap = {
size: DEFAULT_MAP_SIZE, size: DEFAULT_MAP_SIZE,
walls: gameCreationDto.map.walls, walls: gameCreationDto.map.walls
}; }
if (!this.isInAGame(names[0]) && !this.isInAGame(names[1])) { if (!this.isInAGame(names[0]) && !this.isInAGame(names[1])) {
this.games.push( this.games.push(
new Game( new Game(
@ -43,76 +43,76 @@ export class Games {
gameCreationDto.initialBallSpeedY gameCreationDto.initialBallSpeedY
) )
) )
); )
this.playerNameToGameIndex.set(names[0], this.games.length - 1); this.playerNameToGameIndex.set(names[0], this.games.length - 1)
this.playerNameToGameIndex.set(names[1], this.games.length - 1); this.playerNameToGameIndex.set(names[1], this.games.length - 1)
console.log( console.log(
`Created game ${names[0]} vs ${names[1]} (${ `Created game ${names[0]} vs ${names[1]} (${
this.games[this.games.length - 1].id this.games[this.games.length - 1].id
})` })`
); )
} }
} }
ready(name: string): void { ready (name: string): void {
const game: Game | undefined = this.playerGame(name); const game: Game | undefined = this.playerGame(name)
if (game !== undefined) { if (game !== undefined) {
game.ready(name); game.ready(name)
} }
} }
private deleteGame(name: string): void { private deleteGame (name: string): void {
const game: Game | undefined = this.playerGame(name); const game: Game | undefined = this.playerGame(name)
if (game !== undefined) { if (game !== undefined) {
this.games.splice(this.games.indexOf(game), 1); this.games.splice(this.games.indexOf(game), 1)
game.players.forEach((player) => { game.players.forEach((player) => {
this.playerNameToGameIndex.delete(player.name); this.playerNameToGameIndex.delete(player.name)
}); })
console.log(`Game stopped: ${game.id}`); console.log(`Game stopped: ${game.id}`)
} }
} }
getGameInfo(name: string): GameInfo { getGameInfo (name: string): GameInfo {
const game: Game | undefined = this.playerGame(name); const game: Game | undefined = this.playerGame(name)
if (game !== undefined) { if (game !== undefined) {
return game.getGameInfo(name); return game.getGameInfo(name)
} }
return { return {
yourPaddleIndex: -2, yourPaddleIndex: -2,
gameId: "", gameId: '',
mapSize: new Point(0, 0), mapSize: new Point(0, 0),
walls: [], walls: [],
paddleSize: DEFAULT_PADDLE_SIZE, paddleSize: DEFAULT_PADDLE_SIZE,
ballSize: DEFAULT_BALL_SIZE, ballSize: DEFAULT_BALL_SIZE,
winScore: DEFAULT_WIN_SCORE, winScore: DEFAULT_WIN_SCORE,
ranked: false, ranked: false
}; }
} }
movePlayer(name: string | undefined, position: Point): void { movePlayer (name: string | undefined, position: Point): void {
const game: Game | undefined = this.playerGame(name); const game: Game | undefined = this.playerGame(name)
if (game !== undefined) { if (game !== undefined) {
game.movePaddle(name, position); game.movePaddle(name, position)
} }
} }
isInAGame(name: string | undefined): boolean { isInAGame (name: string | undefined): boolean {
if (name === undefined) return false; if (name === undefined) return false
return this.playerNameToGameIndex.get(name) !== undefined; return this.playerNameToGameIndex.get(name) !== undefined
} }
playerGame(name: string | undefined): Game | undefined { playerGame (name: string | undefined): Game | undefined {
const game: Game | undefined = this.games.find((game) => const game: Game | undefined = this.games.find((game) =>
game.players.some((player) => player.name === name) game.players.some((player) => player.name === name)
); )
return game; return game
} }
async leaveGame(name: string): Promise<void> { async leaveGame (name: string): Promise<void> {
const game: Game | undefined = this.playerGame(name); const game: Game | undefined = this.playerGame(name)
if (game !== undefined && !game.ranked) { if (game !== undefined && !game.ranked) {
game.stop(); game.stop()
this.deleteGame(name); this.deleteGame(name)
} }
} }
} }

58
back/volume/src/pong/game/MatchmakingQueue.ts

@ -1,64 +1,64 @@
import { type Socket } from "socket.io"; import { type Socket } from 'socket.io'
import { type GameCreationDtoValidated } from "../dtos/GameCreationDtoValidated"; import { type GameCreationDtoValidated } from '../dtos/GameCreationDtoValidated'
import { DEFAULT_BALL_INITIAL_SPEED, DEFAULT_MAP_SIZE } from "./constants"; import { DEFAULT_BALL_INITIAL_SPEED, DEFAULT_MAP_SIZE } from './constants'
import { type Games } from "./Games"; import { type Games } from './Games'
export class MatchmakingQueue { export class MatchmakingQueue {
games: Games; games: Games
queue: Array<{ name: string; socket: Socket; uuid: string }>; queue: Array<{ name: string, socket: Socket, uuid: string }>
constructor(games: Games) { constructor (games: Games) {
this.games = games; this.games = games
this.queue = []; this.queue = []
} }
addPlayer(name: string, socket: Socket, uuid: string): void { addPlayer (name: string, socket: Socket, uuid: string): void {
if (!this.isInQueue(name)) { if (!this.isInQueue(name)) {
console.log("Adding player to queue: ", name); console.log('Adding player to queue: ', name)
this.queue.push({ name, socket, uuid }); this.queue.push({ name, socket, uuid })
if (this.canCreateGame()) { if (this.canCreateGame()) {
this.createGame(); this.createGame()
} }
} }
} }
removePlayer(name: string): void { removePlayer (name: string): void {
if (this.isInQueue(name)) { if (this.isInQueue(name)) {
console.log("Removing player from queue: ", name); console.log('Removing player from queue: ', name)
this.queue = this.queue.filter((player) => player.name !== name); this.queue = this.queue.filter((player) => player.name !== name)
} }
} }
isInQueue(name: string): boolean { isInQueue (name: string): boolean {
return this.queue.some((player) => player.name === name); return this.queue.some((player) => player.name === name)
} }
canCreateGame(): boolean { canCreateGame (): boolean {
return this.queue.length >= 2; return this.queue.length >= 2
} }
createGame(): void { createGame (): void {
const player1 = this.queue.shift(); const player1 = this.queue.shift()
const player2 = this.queue.shift(); const player2 = this.queue.shift()
if (player1 === undefined || player2 === undefined) { if (player1 === undefined || player2 === undefined) {
return; return
} }
const gameCreationDto: GameCreationDtoValidated = { const gameCreationDto: GameCreationDtoValidated = {
playerNames: [player1.name, player2.name], playerNames: [player1.name, player2.name],
map: { map: {
size: DEFAULT_MAP_SIZE, size: DEFAULT_MAP_SIZE,
walls: [], walls: []
}, },
initialBallSpeedX: DEFAULT_BALL_INITIAL_SPEED.x, initialBallSpeedX: DEFAULT_BALL_INITIAL_SPEED.x,
initialBallSpeedY: DEFAULT_BALL_INITIAL_SPEED.y, initialBallSpeedY: DEFAULT_BALL_INITIAL_SPEED.y
}; }
const ranked = true; const ranked = true
this.games.newGame( this.games.newGame(
[player1.socket, player2.socket], [player1.socket, player2.socket],
[player1.uuid, player2.uuid], [player1.uuid, player2.uuid],
gameCreationDto, gameCreationDto,
ranked ranked
); )
} }
} }

30
back/volume/src/pong/game/Paddle.ts

@ -1,32 +1,32 @@
import { DEFAULT_PADDLE_SIZE } from "./constants"; import { DEFAULT_PADDLE_SIZE } from './constants'
import { type Point, Rect } from "./utils"; import { type Point, Rect } from './utils'
export class Paddle { export class Paddle {
rect: Rect; rect: Rect
color: string | CanvasGradient | CanvasPattern = "white"; color: string | CanvasGradient | CanvasPattern = 'white'
mapSize: Point; mapSize: Point
constructor( constructor (
spawn: Point, spawn: Point,
gameSize: Point, gameSize: Point,
size: Point = DEFAULT_PADDLE_SIZE size: Point = DEFAULT_PADDLE_SIZE
) { ) {
this.rect = new Rect(spawn, size); this.rect = new Rect(spawn, size)
this.mapSize = gameSize; this.mapSize = gameSize
} }
draw(context: CanvasRenderingContext2D): void { draw (context: CanvasRenderingContext2D): void {
this.rect.draw(context, this.color); this.rect.draw(context, this.color)
} }
move(newY: number): void { move (newY: number): void {
const offset: number = this.rect.size.y / 2; const offset: number = this.rect.size.y / 2
if (newY - offset < 0) { if (newY - offset < 0) {
this.rect.center.y = offset; this.rect.center.y = offset
} else if (newY + offset > this.mapSize.y) { } else if (newY + offset > this.mapSize.y) {
this.rect.center.y = this.mapSize.y - offset; this.rect.center.y = this.mapSize.y - offset
} else { } else {
this.rect.center.y = newY; this.rect.center.y = newY
} }
} }
} }

46
back/volume/src/pong/game/Player.ts

@ -1,36 +1,36 @@
import { type Socket } from "socket.io"; import { type Socket } from 'socket.io'
import { Paddle } from "./Paddle"; import { Paddle } from './Paddle'
import { type Point } from "./utils"; import { type Point } from './utils'
export class Player { export class Player {
socket: Socket; socket: Socket
uuid: string; uuid: string
name: string; name: string
ready: boolean; ready: boolean
paddle: Paddle; paddle: Paddle
paddleCoords: Point; paddleCoords: Point
mapSize: Point; mapSize: Point
score: number; score: number
constructor( constructor (
socket: Socket, socket: Socket,
uuid: string, uuid: string,
name: string, name: string,
paddleCoords: Point, paddleCoords: Point,
mapSize: Point mapSize: Point
) { ) {
this.socket = socket; this.socket = socket
this.uuid = uuid; this.uuid = uuid
this.name = name; this.name = name
this.ready = false; this.ready = false
this.paddle = new Paddle(paddleCoords, mapSize); this.paddle = new Paddle(paddleCoords, mapSize)
this.paddleCoords = paddleCoords; this.paddleCoords = paddleCoords
this.mapSize = mapSize; this.mapSize = mapSize
this.score = 0; this.score = 0
} }
newGame(): void { newGame (): void {
this.score = 0; this.score = 0
this.paddle = new Paddle(this.paddleCoords, this.mapSize); this.paddle = new Paddle(this.paddleCoords, this.mapSize)
} }
} }

38
back/volume/src/pong/game/constants.ts

@ -1,22 +1,22 @@
import { Point } from "./utils"; import { Point } from './utils'
export const GAME_EVENTS = { export const GAME_EVENTS = {
START_GAME: "START_GAME", START_GAME: 'START_GAME',
READY: "READY", READY: 'READY',
GAME_TICK: "GAME_TICK", GAME_TICK: 'GAME_TICK',
PLAYER_MOVE: "PLAYER_MOVE", PLAYER_MOVE: 'PLAYER_MOVE',
GET_GAME_INFO: "GET_GAME_INFO", GET_GAME_INFO: 'GET_GAME_INFO',
CREATE_GAME: "CREATE_GAME", CREATE_GAME: 'CREATE_GAME',
REGISTER_PLAYER: "REGISTER_PLAYER", REGISTER_PLAYER: 'REGISTER_PLAYER',
MATCHMAKING: "MATCHMAKING", MATCHMAKING: 'MATCHMAKING',
LEAVE_GAME: "LEAVE_GAME", LEAVE_GAME: 'LEAVE_GAME'
}; }
export const DEFAULT_MAP_SIZE = new Point(500, 400); export const DEFAULT_MAP_SIZE = new Point(500, 400)
export const DEFAULT_PADDLE_SIZE = new Point(30, 50); export const DEFAULT_PADDLE_SIZE = new Point(30, 50)
export const DEFAULT_BALL_SIZE = new Point(10, 10); export const DEFAULT_BALL_SIZE = new Point(10, 10)
export const DEFAULT_BALL_INITIAL_SPEED = new Point(10, 2); export const DEFAULT_BALL_INITIAL_SPEED = new Point(10, 2)
export const DEFAULT_MAX_BALL_SPEED = new Point(30, 20); export const DEFAULT_MAX_BALL_SPEED = new Point(30, 20)
export const DEFAULT_BALL_SPEED_INCREMENT = new Point(0.05, 0); export const DEFAULT_BALL_SPEED_INCREMENT = new Point(0.05, 0)
export const DEFAULT_WIN_SCORE = 5; export const DEFAULT_WIN_SCORE = 5
export const GAME_TICKS = 30; export const GAME_TICKS = 30

72
back/volume/src/pong/game/utils.ts

@ -1,83 +1,83 @@
export class Point { export class Point {
x: number; x: number
y: number; y: number
constructor(x: number, y: number) { constructor (x: number, y: number) {
this.x = x; this.x = x
this.y = y; this.y = y
} }
// Returns a new point // Returns a new point
add(other: Point): Point { add (other: Point): Point {
return new Point(this.x + other.x, this.y + other.y); return new Point(this.x + other.x, this.y + other.y)
} }
// Modifies `this` point // Modifies `this` point
add_inplace(other: Point): void { add_inplace (other: Point): void {
this.x += other.x; this.x += other.x
this.y += other.y; this.y += other.y
} }
clone(): Point { clone (): Point {
return new Point(this.x, this.y); return new Point(this.x, this.y)
} }
} }
export class Rect { export class Rect {
center: Point; center: Point
size: Point; size: Point
constructor(center: Point, size: Point) { constructor (center: Point, size: Point) {
this.center = center; this.center = center
this.size = size; this.size = size
} }
draw( draw (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
color: string | CanvasGradient | CanvasPattern color: string | CanvasGradient | CanvasPattern
): void { ): void {
const offset: Point = new Point(this.size.x / 2, this.size.y / 2); const offset: Point = new Point(this.size.x / 2, this.size.y / 2)
context.fillStyle = color; context.fillStyle = color
context.fillRect( context.fillRect(
this.center.x - offset.x, this.center.x - offset.x,
this.center.y - offset.y, this.center.y - offset.y,
this.size.x, this.size.x,
this.size.y this.size.y
); )
} }
// True if `this` rect contains `other` rect in the x-axis // True if `this` rect contains `other` rect in the x-axis
contains_x(other: Rect): boolean { contains_x (other: Rect): boolean {
const offset: number = this.size.x / 2; const offset: number = this.size.x / 2
const offsetOther: number = other.size.x / 2; const offsetOther: number = other.size.x / 2
if ( if (
this.center.x - offset <= other.center.x - offsetOther && this.center.x - offset <= other.center.x - offsetOther &&
this.center.x + offset >= other.center.x + offsetOther this.center.x + offset >= other.center.x + offsetOther
) { ) {
return true; return true
} }
return false; return false
} }
// True if `this` rect contains `other` rect in the y-axis // True if `this` rect contains `other` rect in the y-axis
contains_y(other: Rect): boolean { contains_y (other: Rect): boolean {
const offset: number = this.size.y / 2; const offset: number = this.size.y / 2
const offsetOther: number = other.size.y / 2; const offsetOther: number = other.size.y / 2
if ( if (
this.center.y - offset <= other.center.y - offsetOther && this.center.y - offset <= other.center.y - offsetOther &&
this.center.y + offset >= other.center.y + offsetOther this.center.y + offset >= other.center.y + offsetOther
) { ) {
return true; return true
} }
return false; return false
} }
collides(other: Rect): boolean { collides (other: Rect): boolean {
const offset: Point = new Point(this.size.x / 2, this.size.y / 2); const offset: Point = new Point(this.size.x / 2, this.size.y / 2)
const offsetOther: Point = new Point(other.size.x / 2, other.size.y / 2); const offsetOther: Point = new Point(other.size.x / 2, other.size.y / 2)
if ( if (
this.center.x - offset.x < other.center.x + offsetOther.x && this.center.x - offset.x < other.center.x + offsetOther.x &&
@ -85,8 +85,8 @@ export class Rect {
this.center.y - offset.y < other.center.y + offsetOther.y && this.center.y - offset.y < other.center.y + offsetOther.y &&
this.center.y + offset.y > other.center.y - offsetOther.y this.center.y + offset.y > other.center.y - offsetOther.y
) { ) {
return true; return true
} }
return false; return false
} }
} }

30
back/volume/src/pong/pong.controller.ts

@ -3,31 +3,31 @@ import {
Get, Get,
Param, Param,
ParseIntPipe, ParseIntPipe,
UseGuards, UseGuards
} from "@nestjs/common"; } from '@nestjs/common'
import { Paginate, type Paginated, PaginateQuery } from "nestjs-paginate"; import { Paginate, type Paginated, PaginateQuery } from 'nestjs-paginate'
import { AuthenticatedGuard } from "src/auth/42-auth.guard"; import { AuthenticatedGuard } from 'src/auth/42-auth.guard'
import type Result from "./entity/result.entity"; import type Result from './entity/result.entity'
import { PongService } from "./pong.service"; import { PongService } from './pong.service'
@Controller("results") @Controller('results')
export class PongController { export class PongController {
constructor(private readonly pongService: PongService) {} constructor (private readonly pongService: PongService) {}
@Get("global") @Get('global')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getGlobalHistory( async getGlobalHistory (
@Paginate() query: PaginateQuery @Paginate() query: PaginateQuery
): Promise<Paginated<Result>> { ): Promise<Paginated<Result>> {
return await this.pongService.getHistory(query, 0); return await this.pongService.getHistory(query, 0)
} }
@Get(":id") @Get(':id')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getHistoryById( async getHistoryById (
@Param("id", ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Paginate() query: PaginateQuery @Paginate() query: PaginateQuery
): Promise<Paginated<Result>> { ): Promise<Paginated<Result>> {
return await this.pongService.getHistory(query, id); return await this.pongService.getHistory(query, id)
} }
} }

24
back/volume/src/pong/pong.gateway.spec.ts

@ -1,18 +1,18 @@
import { Test, type TestingModule } from "@nestjs/testing"; import { Test, type TestingModule } from '@nestjs/testing'
import { PongGateway } from "./pong.gateway"; import { PongGateway } from './pong.gateway'
describe("PongGateway", () => { describe('PongGateway', () => {
let gateway: PongGateway; let gateway: PongGateway
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [PongGateway], providers: [PongGateway]
}).compile(); }).compile()
gateway = module.get<PongGateway>(PongGateway); gateway = module.get<PongGateway>(PongGateway)
}); })
it("should be defined", () => { it('should be defined', () => {
expect(gateway).toBeDefined(); expect(gateway).toBeDefined()
}); })
}); })

158
back/volume/src/pong/pong.gateway.ts

@ -1,96 +1,96 @@
import { UsePipes, ValidationPipe } from "@nestjs/common"; import { UsePipes, ValidationPipe } from '@nestjs/common'
import { Socket } from "socket.io"; import { Socket } from 'socket.io'
import { import {
ConnectedSocket, ConnectedSocket,
MessageBody, MessageBody,
type OnGatewayConnection, type OnGatewayConnection,
type OnGatewayDisconnect, type OnGatewayDisconnect,
SubscribeMessage, SubscribeMessage,
WebSocketGateway, WebSocketGateway
} from "@nestjs/websockets"; } from '@nestjs/websockets'
import { Games } from "./game/Games"; import { Games } from './game/Games'
import { GAME_EVENTS } from "./game/constants"; import { GAME_EVENTS } from './game/constants'
import { GameCreationDtoValidated } from "./dtos/GameCreationDtoValidated"; import { GameCreationDtoValidated } from './dtos/GameCreationDtoValidated'
import { type Game } from "./game/Game"; import { type Game } from './game/Game'
import { plainToClass } from "class-transformer"; import { plainToClass } from 'class-transformer'
import { PointDtoValidated } from "./dtos/PointDtoValidated"; import { PointDtoValidated } from './dtos/PointDtoValidated'
import { StringDtoValidated } from "./dtos/StringDtoValidated"; import { StringDtoValidated } from './dtos/StringDtoValidated'
import { MatchmakingQueue } from "./game/MatchmakingQueue"; import { MatchmakingQueue } from './game/MatchmakingQueue'
import { MatchmakingDtoValidated } from "./dtos/MatchmakingDtoValidated"; import { MatchmakingDtoValidated } from './dtos/MatchmakingDtoValidated'
import { PongService } from "./pong.service"; import { PongService } from './pong.service'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
@WebSocketGateway({ @WebSocketGateway({
cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ }, cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ }
}) })
export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor( constructor (
private readonly pongService: PongService, private readonly pongService: PongService,
private readonly usersService: UsersService private readonly usersService: UsersService
) {} ) {}
private readonly games: Games = new Games(this.pongService); private readonly games: Games = new Games(this.pongService)
private readonly socketToPlayerName = new Map<Socket, string>(); private readonly socketToPlayerName = new Map<Socket, string>()
private readonly matchmakingQueue = new MatchmakingQueue(this.games); private readonly matchmakingQueue = new MatchmakingQueue(this.games)
playerIsRegistered(name: string): boolean { playerIsRegistered (name: string): boolean {
return Array.from(this.socketToPlayerName.values()).includes(name); return Array.from(this.socketToPlayerName.values()).includes(name)
} }
handleConnection(): void {} handleConnection (): void {}
handleDisconnect( handleDisconnect (
@ConnectedSocket() @ConnectedSocket()
client: Socket client: Socket
): void { ): void {
const name: string | undefined = this.socketToPlayerName.get(client); const name: string | undefined = this.socketToPlayerName.get(client)
const game: Game | undefined = this.games.playerGame(name); const game: Game | undefined = this.games.playerGame(name)
if (game !== undefined) { if (game !== undefined) {
game.stop(); game.stop()
} }
if (name !== undefined) { if (name !== undefined) {
console.log("Disconnected ", this.socketToPlayerName.get(client)); console.log('Disconnected ', this.socketToPlayerName.get(client))
this.matchmakingQueue.removePlayer(name); this.matchmakingQueue.removePlayer(name)
this.socketToPlayerName.delete(client); this.socketToPlayerName.delete(client)
} }
} }
@UsePipes(new ValidationPipe({ whitelist: true })) @UsePipes(new ValidationPipe({ whitelist: true }))
@SubscribeMessage(GAME_EVENTS.REGISTER_PLAYER) @SubscribeMessage(GAME_EVENTS.REGISTER_PLAYER)
async registerPlayer( async registerPlayer (
@ConnectedSocket() @ConnectedSocket()
client: Socket, client: Socket,
@MessageBody("playerName") playerName: StringDtoValidated, @MessageBody('playerName') playerName: StringDtoValidated,
@MessageBody("socketKey") socketKey: StringDtoValidated @MessageBody('socketKey') socketKey: StringDtoValidated
): Promise<{ event: string; data: boolean }> { ): Promise<{ event: string, data: boolean }> {
let succeeded: boolean = false; let succeeded: boolean = false
const user = await this.usersService.findUserByName(playerName.value); const user = await this.usersService.findUserByName(playerName.value)
if ( if (
user !== null && user !== null &&
user.socketKey === socketKey.value && user.socketKey === socketKey.value &&
!this.playerIsRegistered(playerName.value) !this.playerIsRegistered(playerName.value)
) { ) {
this.socketToPlayerName.set(client, playerName.value); this.socketToPlayerName.set(client, playerName.value)
succeeded = true; succeeded = true
console.log("Registered player", playerName.value); console.log('Registered player', playerName.value)
} else { } else {
console.log("Failed to register player", playerName.value); console.log('Failed to register player', playerName.value)
} }
return { event: GAME_EVENTS.REGISTER_PLAYER, data: succeeded }; return { event: GAME_EVENTS.REGISTER_PLAYER, data: succeeded }
} }
@SubscribeMessage(GAME_EVENTS.GET_GAME_INFO) @SubscribeMessage(GAME_EVENTS.GET_GAME_INFO)
getPlayerCount(@ConnectedSocket() client: Socket): void { getPlayerCount (@ConnectedSocket() client: Socket): void {
const name: string | undefined = this.socketToPlayerName.get(client); const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) { if (name !== undefined) {
client.emit(GAME_EVENTS.GET_GAME_INFO, this.games.getGameInfo(name)); client.emit(GAME_EVENTS.GET_GAME_INFO, this.games.getGameInfo(name))
} }
} }
@UsePipes(new ValidationPipe({ whitelist: true })) @UsePipes(new ValidationPipe({ whitelist: true }))
@SubscribeMessage(GAME_EVENTS.PLAYER_MOVE) @SubscribeMessage(GAME_EVENTS.PLAYER_MOVE)
movePlayer( movePlayer (
@ConnectedSocket() @ConnectedSocket()
client: Socket, client: Socket,
@MessageBody() position: PointDtoValidated @MessageBody() position: PointDtoValidated
@ -98,22 +98,22 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
const realPosition: PointDtoValidated = plainToClass( const realPosition: PointDtoValidated = plainToClass(
PointDtoValidated, PointDtoValidated,
position position
); )
const name: string | undefined = this.socketToPlayerName.get(client); const name: string | undefined = this.socketToPlayerName.get(client)
this.games.movePlayer(name, realPosition); this.games.movePlayer(name, realPosition)
} }
@UsePipes(new ValidationPipe({ whitelist: true })) @UsePipes(new ValidationPipe({ whitelist: true }))
@SubscribeMessage(GAME_EVENTS.CREATE_GAME) @SubscribeMessage(GAME_EVENTS.CREATE_GAME)
createGame( createGame (
@ConnectedSocket() @ConnectedSocket()
client: Socket, client: Socket,
@MessageBody() gameCreationDto: GameCreationDtoValidated @MessageBody() gameCreationDto: GameCreationDtoValidated
): { event: string; data: boolean } { ): { event: string, data: boolean } {
const realGameCreationDto: GameCreationDtoValidated = plainToClass( const realGameCreationDto: GameCreationDtoValidated = plainToClass(
GameCreationDtoValidated, GameCreationDtoValidated,
gameCreationDto gameCreationDto
); )
if (this.socketToPlayerName.size >= 2) { if (this.socketToPlayerName.size >= 2) {
const player1Socket: Socket | undefined = Array.from( const player1Socket: Socket | undefined = Array.from(
@ -122,14 +122,14 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
(key) => (key) =>
this.socketToPlayerName.get(key) === this.socketToPlayerName.get(key) ===
realGameCreationDto.playerNames[0] realGameCreationDto.playerNames[0]
); )
const player2Socket: Socket | undefined = Array.from( const player2Socket: Socket | undefined = Array.from(
this.socketToPlayerName.keys() this.socketToPlayerName.keys()
).find( ).find(
(key) => (key) =>
this.socketToPlayerName.get(key) === this.socketToPlayerName.get(key) ===
realGameCreationDto.playerNames[1] realGameCreationDto.playerNames[1]
); )
if ( if (
player1Socket !== undefined && player1Socket !== undefined &&
@ -137,68 +137,68 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
(client.id === player1Socket.id || client.id === player2Socket.id) && (client.id === player1Socket.id || client.id === player2Socket.id) &&
player1Socket.id !== player2Socket.id player1Socket.id !== player2Socket.id
) { ) {
this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[0]); this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[0])
this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[1]); this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[1])
const ranked = false; const ranked = false
this.games.newGame( this.games.newGame(
[player1Socket, player2Socket], [player1Socket, player2Socket],
[player1Socket.id, player2Socket.id], [player1Socket.id, player2Socket.id],
realGameCreationDto, realGameCreationDto,
ranked ranked
); )
return { event: GAME_EVENTS.CREATE_GAME, data: true }; return { event: GAME_EVENTS.CREATE_GAME, data: true }
} }
} }
return { event: GAME_EVENTS.CREATE_GAME, data: false }; return { event: GAME_EVENTS.CREATE_GAME, data: false }
} }
@SubscribeMessage(GAME_EVENTS.READY) @SubscribeMessage(GAME_EVENTS.READY)
ready( ready (
@ConnectedSocket() @ConnectedSocket()
client: Socket client: Socket
): { event: string; data: boolean } { ): { event: string, data: boolean } {
let succeeded: boolean = false; let succeeded: boolean = false
const name: string | undefined = this.socketToPlayerName.get(client); const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) { if (name !== undefined) {
this.games.ready(name); this.games.ready(name)
succeeded = true; succeeded = true
} }
return { event: GAME_EVENTS.READY, data: succeeded }; return { event: GAME_EVENTS.READY, data: succeeded }
} }
@UsePipes(new ValidationPipe({ whitelist: true })) @UsePipes(new ValidationPipe({ whitelist: true }))
@SubscribeMessage(GAME_EVENTS.MATCHMAKING) @SubscribeMessage(GAME_EVENTS.MATCHMAKING)
updateMatchmaking( updateMatchmaking (
@ConnectedSocket() @ConnectedSocket()
client: Socket, client: Socket,
@MessageBody() matchmakingUpdateData: MatchmakingDtoValidated @MessageBody() matchmakingUpdateData: MatchmakingDtoValidated
): { event: string; data: MatchmakingDtoValidated } { ): { event: string, data: MatchmakingDtoValidated } {
let matchmaking: boolean = false; let matchmaking: boolean = false
const name: string | undefined = this.socketToPlayerName.get(client); const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) { if (name !== undefined) {
if (matchmakingUpdateData.matchmaking && !this.games.isInAGame(name)) { if (matchmakingUpdateData.matchmaking && !this.games.isInAGame(name)) {
this.matchmakingQueue.addPlayer(name, client, client.id); this.matchmakingQueue.addPlayer(name, client, client.id)
} else { } else {
this.matchmakingQueue.removePlayer(name); this.matchmakingQueue.removePlayer(name)
} }
matchmaking = this.matchmakingQueue.isInQueue(name); matchmaking = this.matchmakingQueue.isInQueue(name)
} }
return { return {
event: GAME_EVENTS.MATCHMAKING, event: GAME_EVENTS.MATCHMAKING,
data: { matchmaking }, data: { matchmaking }
}; }
} }
@UsePipes(new ValidationPipe({ whitelist: true })) @UsePipes(new ValidationPipe({ whitelist: true }))
@SubscribeMessage(GAME_EVENTS.LEAVE_GAME) @SubscribeMessage(GAME_EVENTS.LEAVE_GAME)
leaveGame( leaveGame (
@ConnectedSocket() @ConnectedSocket()
client: Socket client: Socket
): void { ): void {
const name: string | undefined = this.socketToPlayerName.get(client); const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) { if (name !== undefined) {
void this.games.leaveGame(name); void this.games.leaveGame(name)
} }
} }
} }

16
back/volume/src/pong/pong.module.ts

@ -1,15 +1,15 @@
import { forwardRef, Module } from "@nestjs/common"; import { forwardRef, Module } from '@nestjs/common'
import { PongGateway } from "./pong.gateway"; import { PongGateway } from './pong.gateway'
import Result from "./entity/result.entity"; import Result from './entity/result.entity'
import { TypeOrmModule } from "@nestjs/typeorm"; import { TypeOrmModule } from '@nestjs/typeorm'
import { PongService } from "./pong.service"; import { PongService } from './pong.service'
import { UsersModule } from "src/users/users.module"; import { UsersModule } from 'src/users/users.module'
import { PongController } from "./pong.controller"; import { PongController } from './pong.controller'
@Module({ @Module({
imports: [forwardRef(() => UsersModule), TypeOrmModule.forFeature([Result])], imports: [forwardRef(() => UsersModule), TypeOrmModule.forFeature([Result])],
providers: [PongGateway, PongService], providers: [PongGateway, PongService],
controllers: [PongController], controllers: [PongController],
exports: [PongService], exports: [PongService]
}) })
export class PongModule {} export class PongModule {}

96
back/volume/src/pong/pong.service.ts

@ -1,89 +1,89 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common'
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from "typeorm"; import { Repository } from 'typeorm'
import { UsersService } from "src/users/users.service"; import { UsersService } from 'src/users/users.service'
import Result from "./entity/result.entity"; import Result from './entity/result.entity'
import type User from "src/users/entity/user.entity"; import type User from 'src/users/entity/user.entity'
import { type Player } from "./game/Player"; import { type Player } from './game/Player'
import { type PaginateQuery, paginate, type Paginated } from "nestjs-paginate"; import { type PaginateQuery, paginate, type Paginated } from 'nestjs-paginate'
@Injectable() @Injectable()
export class PongService { export class PongService {
constructor( constructor (
@InjectRepository(Result) @InjectRepository(Result)
private readonly resultsRepository: Repository<Result>, private readonly resultsRepository: Repository<Result>,
private readonly usersService: UsersService private readonly usersService: UsersService
) {} ) {}
async updateStats( async updateStats (
player: User, player: User,
i: number, i: number,
result: Result, result: Result,
maxScore: number maxScore: number
): Promise<void> { ): Promise<void> {
player.matchs++; player.matchs++
if (result.score[i] === maxScore) player.wins++; if (result.score[i] === maxScore) player.wins++
else player.looses++; else player.looses++
player.winrate = (100 * player.wins) / player.matchs; player.winrate = (100 * player.wins) / player.matchs
} }
async updatePlayer( async updatePlayer (
i: number, i: number,
result: Result, result: Result,
maxScore: number maxScore: number
): Promise<void> { ): Promise<void> {
const player: User | null = result.players[i]; const player: User | null = result.players[i]
if (player == null) return; if (player == null) return
if (result.ranked) await this.updateStats(player, i, result, maxScore); if (result.ranked) await this.updateStats(player, i, result, maxScore)
player.results.push(result); player.results.push(result)
player.status = "online"; player.status = 'online'
await this.usersService.save(player); await this.usersService.save(player)
} }
async setInGame(playerName: string): Promise<void> { async setInGame (playerName: string): Promise<void> {
const player = await this.usersService.findUserByName(playerName); const player = await this.usersService.findUserByName(playerName)
player.status = "in-game"; player.status = 'in-game'
await this.usersService.save(player); await this.usersService.save(player)
} }
async saveResult( async saveResult (
players: Player[], players: Player[],
ranked: boolean, ranked: boolean,
maxScore: number maxScore: number
): Promise<void> { ): Promise<void> {
const result = new Result(); const result = new Result()
const ply = new Array<User | null>(); const ply = new Array<User | null>()
ply.push(await this.usersService.findUserByName(players[0].name)); ply.push(await this.usersService.findUserByName(players[0].name))
ply.push(await this.usersService.findUserByName(players[1].name)); ply.push(await this.usersService.findUserByName(players[1].name))
result.ranked = ranked; result.ranked = ranked
result.players = ply; result.players = ply
result.score = [players[0].score, players[1].score]; result.score = [players[0].score, players[1].score]
await this.resultsRepository.save(result); await this.resultsRepository.save(result)
await this.updatePlayer(0, result, maxScore); await this.updatePlayer(0, result, maxScore)
await this.updatePlayer(1, result, maxScore); await this.updatePlayer(1, result, maxScore)
} }
async getHistory( async getHistory (
query: PaginateQuery, query: PaginateQuery,
ftId: number ftId: number
): Promise<Paginated<Result>> { ): Promise<Paginated<Result>> {
let queryBuilder; let queryBuilder
if (ftId !== 0) { if (ftId !== 0) {
queryBuilder = this.resultsRepository queryBuilder = this.resultsRepository
.createQueryBuilder("result") .createQueryBuilder('result')
.innerJoin("result.players", "player", "player.ftId = :ftId", { ftId }); .innerJoin('result.players', 'player', 'player.ftId = :ftId', { ftId })
} else { } else {
queryBuilder = this.resultsRepository queryBuilder = this.resultsRepository
.createQueryBuilder("result") .createQueryBuilder('result')
.where("result.ranked = :ranked", { ranked: true }); .where('result.ranked = :ranked', { ranked: true })
} }
return await paginate(query, queryBuilder, { return await paginate(query, queryBuilder, {
nullSort: "last", nullSort: 'last',
relations: ["players"], relations: ['players'],
defaultSortBy: [["date", "DESC"]], defaultSortBy: [['date', 'DESC']],
sortableColumns: ["date"], sortableColumns: ['date'],
maxLimit: 10, maxLimit: 10
}); })
} }
} }

24
back/volume/src/pong/pong.spec.ts

@ -1,18 +1,18 @@
import { Test, type TestingModule } from "@nestjs/testing"; import { Test, type TestingModule } from '@nestjs/testing'
import { Games } from "./game/Games"; import { Games } from './game/Games'
describe("Pong", () => { describe('Pong', () => {
let provider: Games; let provider: Games
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [Games], providers: [Games]
}).compile(); }).compile()
provider = module.get<Games>(Games); provider = module.get<Games>(Games)
}); })
it("should be defined", () => { it('should be defined', () => {
expect(provider).toBeDefined(); expect(provider).toBeDefined()
}); })
}); })

10
back/volume/src/types.d.ts

@ -1,8 +1,8 @@
declare module "passport-42" { declare module 'passport-42' {
export type Profile = any; export type Profile = any
export type VerifyCallback = any; export type VerifyCallback = any
export class Strategy { export class Strategy {
constructor(options: any, verify: any); constructor (options: any, verify: any)
authenticate(req: any, options: any): any; authenticate (req: any, options: any): any
} }
} }

22
back/volume/src/users/dto/user.dto.ts

@ -1,31 +1,31 @@
import { IsString, IsNotEmpty, IsPositive, IsOptional } from "class-validator"; import { IsString, IsNotEmpty, IsPositive, IsOptional } from 'class-validator'
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger'
import { Express } from "express"; import { Express } from 'express'
export class UserDto { export class UserDto {
@IsPositive() @IsPositive()
@IsOptional() @IsOptional()
readonly ftId: number; readonly ftId: number
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
readonly username: string; readonly username: string
@IsOptional() @IsOptional()
readonly status: string; readonly status: string
@IsOptional() @IsOptional()
readonly avatar: string; readonly avatar: string
@IsOptional() @IsOptional()
readonly authToken: string; readonly authToken: string
@IsOptional() @IsOptional()
readonly isVerified: boolean; readonly isVerified: boolean
} }
export class AvatarUploadDto { export class AvatarUploadDto {
@ApiProperty({ type: "string", format: "binary" }) @ApiProperty({ type: 'string', format: 'binary' })
file: Express.Multer.File; file: Express.Multer.File
} }

62
back/volume/src/users/entity/user.entity.ts

@ -4,78 +4,78 @@ import {
Column, Column,
OneToMany, OneToMany,
ManyToMany, ManyToMany,
JoinTable, JoinTable
} from "typeorm"; } from 'typeorm'
import Message from "src/chat/entity/message.entity"; import Message from 'src/chat/entity/message.entity'
import Channel from "src/chat/entity/channel.entity"; import Channel from 'src/chat/entity/channel.entity'
import Result from "src/pong/entity/result.entity"; import Result from 'src/pong/entity/result.entity'
@Entity() @Entity()
export class User { export class User {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number
@Column({ type: "bigint", default: Date.now() }) @Column({ type: 'bigint', default: Date.now() })
lastAccess: number; lastAccess: number
@Column({ unique: true }) @Column({ unique: true })
ftId: number; ftId: number
@Column({ unique: true, nullable: true }) @Column({ unique: true, nullable: true })
email: string; email: string
@Column({ select: false, nullable: true }) @Column({ select: false, nullable: true })
authToken: string; authToken: string
@Column({ default: false }) @Column({ default: false })
twoFA: boolean; twoFA: boolean
@Column({ default: false, nullable: true }) @Column({ default: false, nullable: true })
isVerified: boolean; isVerified: boolean
@Column("uuid", { unique: true }) @Column('uuid', { unique: true })
socketKey: string; socketKey: string
@Column({ unique: true }) @Column({ unique: true })
username: string; username: string
@Column({ default: "online" }) @Column({ default: 'online' })
status: string; status: string
@Column({ name: "avatar" }) @Column({ name: 'avatar' })
avatar: string; avatar: string
@Column({ default: 0 }) @Column({ default: 0 })
wins: number; wins: number
@Column({ default: 0 }) @Column({ default: 0 })
looses: number; looses: number
@Column({ default: 0 }) @Column({ default: 0 })
matchs: number; matchs: number
@Column({ default: 0 }) @Column({ default: 0 })
rank: number; rank: number
@Column({ default: 0, type: "double precision" }) @Column({ default: 0, type: 'double precision' })
winrate: number; winrate: number
@ManyToMany(() => Result, (result: Result) => result.players) @ManyToMany(() => Result, (result: Result) => result.players)
@JoinTable() @JoinTable()
results: Result[]; results: Result[]
@ManyToMany(() => User) @ManyToMany(() => User)
@JoinTable() @JoinTable()
blocked: User[]; blocked: User[]
@ManyToMany(() => User) @ManyToMany(() => User)
@JoinTable() @JoinTable()
followers: User[]; followers: User[]
@ManyToMany(() => User) @ManyToMany(() => User)
@JoinTable() @JoinTable()
friends: User[]; friends: User[]
} }
export default User; export default User

198
back/volume/src/users/users.controller.ts

@ -11,46 +11,48 @@ import {
Res, Res,
StreamableFile, StreamableFile,
BadRequestException, BadRequestException,
Redirect, Redirect
} from "@nestjs/common"; } from '@nestjs/common'
import { FileInterceptor } from "@nestjs/platform-express"; import { FileInterceptor } from '@nestjs/platform-express'
import { diskStorage } from "multer"; import { diskStorage } from 'multer'
import { type User } from "./entity/user.entity"; import { type User } from './entity/user.entity'
import { UsersService } from "./users.service"; import { UsersService } from './users.service'
import { UserDto, AvatarUploadDto } from "./dto/user.dto"; import { UserDto, AvatarUploadDto } from './dto/user.dto'
import { PongService } from "src/pong/pong.service";
import { AuthenticatedGuard } from "src/auth/42-auth.guard"; import { AuthenticatedGuard } from 'src/auth/42-auth.guard'
import { Profile42 } from "src/auth/42.decorator"; import { Profile42 } from 'src/auth/42.decorator'
import { Profile } from "passport-42"; import { Profile } from 'passport-42'
import { ApiBody, ApiConsumes } from "@nestjs/swagger"; import { ApiBody, ApiConsumes } from '@nestjs/swagger'
import { type Request, Response } from "express"; import { type Request, Response } from 'express'
import { createReadStream } from "fs"; import { createReadStream } from 'fs'
import { join } from "path"; import { join } from 'path'
@Controller("users") @Controller('users')
export class UsersController { export class UsersController {
constructor(private readonly usersService: UsersService) {} constructor (private readonly usersService: UsersService) {}
@Post('block/:id')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
@Post("block/:id") async blockUser (
async blockUser(@Profile42() profile :Profile, @Param('id') id:number) { @Profile42() profile: Profile,
const user = await this.usersService.findUser(id) as User @Param('id', ParseIntPipe) id: number
) {
const user = (await this.usersService.findUser(id)) as User
user.blocked.push((await this.usersService.findUser(+profile.id)) as User) user.blocked.push((await this.usersService.findUser(+profile.id)) as User)
this.usersService.save(user); this.usersService.save(user)
} }
@Post('unblock/:id')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
@Post("unblock/:id") async unblockUser (@Param('id', ParseIntPipe) id: number) {
async unblockUser(@Profile42() profile :Profile, @Param('id') id:number) { const user = (await this.usersService.findUser(id)) as User
const user = await this.usersService.findUser(id) as User
user.blocked = user.blocked.filter((usr: User) => { user.blocked = user.blocked.filter((usr: User) => {
return usr.id !== id return usr.id !== id
}) })
this.usersService.save(user); this.usersService.save(user)
} }
@Get('all') @Get('all')
@ -58,151 +60,145 @@ export class UsersController {
return await this.usersService.findUsers() return await this.usersService.findUsers()
} }
@Get("online") @Get('online')
async getOnlineUsers(): Promise<User[]> { async getOnlineUsers (): Promise<User[]> {
return await this.usersService.findOnlineUsers(); return await this.usersService.findOnlineUsers()
} }
@Get("friends") @Get('friends')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getFriends(@Profile42() profile: Profile): Promise<User[]> { async getFriends (@Profile42() profile: Profile): Promise<User[]> {
return await this.usersService.getFriends(profile.id); return await this.usersService.getFriends(profile.id)
} }
@Get("invits") @Get('invits')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getInvits(@Profile42() profile: Profile): Promise<User[]> { async getInvits (@Profile42() profile: Profile): Promise<User[]> {
return await this.usersService.getInvits(profile.id); return await this.usersService.getInvits(profile.id)
} }
@Get("leaderboard") @Get('leaderboard')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getLeaderboard(): Promise<User[]> { async getLeaderboard (): Promise<User[]> {
return await this.usersService.getLeaderboard(); return await this.usersService.getLeaderboard()
} }
@Post("avatar") @Post('avatar')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
@Redirect("http://localhost") @Redirect('http://localhost')
@UseInterceptors( @UseInterceptors(
FileInterceptor("avatar", { FileInterceptor('avatar', {
storage: diskStorage({ storage: diskStorage({
destination: "avatars/", destination: 'avatars/'
}), }),
fileFilter: (request: Request, file: Express.Multer.File, callback) => { fileFilter: (request: Request, file: Express.Multer.File, callback) => {
<<<<<<< HEAD
if (!file.mimetype.includes('image')) { if (!file.mimetype.includes('image')) {
callback(null, false) callback(null, false)
return return
======= }
if (!file.mimetype.includes("image")) { callback(null, true)
callback(null, false); }
return;
>>>>>>> ouai c un peu la merde mais bon
}
callback(null, true);
},
}) })
) )
@ApiConsumes("multipart/form-data") @ApiConsumes('multipart/form-data')
@ApiBody({ @ApiBody({
description: "A new avatar for the user", description: 'A new avatar for the user',
type: AvatarUploadDto, type: AvatarUploadDto
}) })
async changeAvatar( async changeAvatar (
@Profile42() profile: Profile, @Profile42() profile: Profile,
@UploadedFile() file: Express.Multer.File @UploadedFile() file: Express.Multer.File
): Promise<void> { ): Promise<void> {
if (file === undefined) return; if (file === undefined) return
await this.usersService.addAvatar(profile.id, file.filename); await this.usersService.addAvatar(profile.id, file.filename)
} }
@Get("avatar") @Get('avatar')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getAvatar( async getAvatar (
@Profile42() profile: Profile, @Profile42() profile: Profile,
@Res({ passthrough: true }) response: Response @Res({ passthrough: true }) response: Response
): Promise<StreamableFile> { ): Promise<StreamableFile> {
return await this.getAvatarById(profile.id, response); return await this.getAvatarById(profile.id, response)
} }
@Get(":name/byname") @Get(':name/byname')
async getUserByName(@Param("name") username: string): Promise<User> { async getUserByName (@Param('name') username: string): Promise<User> {
const user = await this.usersService.findUserByName(username); const user = await this.usersService.findUserByName(username)
user.socketKey = ""; user.socketKey = ''
return user; return user
} }
@Get("invit/:username") @Get('invit/:username')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async invitUser( async invitUser (
@Profile42() profile: Profile, @Profile42() profile: Profile,
@Param("username") username: string @Param('username') username: string
): Promise<void> { ): Promise<void> {
const target: User | null = await this.usersService.findUserByName( const target: User | null = await this.usersService.findUserByName(
username username
); )
if (target === null) { if (target === null) {
throw new BadRequestException(`User ${username} not found.`); 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."); throw new BadRequestException("You can't invite yourself.")
} }
const ret: string = await this.usersService.invit(profile.id, target.ftId); const ret: string = await this.usersService.invit(profile.id, target.ftId)
if (ret !== "OK") throw new BadRequestException(ret); if (ret !== 'OK') throw new BadRequestException(ret)
} }
@Get(":id/avatar") @Get(':id/avatar')
async getAvatarById( async getAvatarById (
@Param("id", ParseIntPipe) ftId: number, @Param('id', ParseIntPipe) ftId: number,
@Res({ passthrough: true }) response: Response @Res({ passthrough: true }) response: Response
): Promise<StreamableFile> { ): Promise<StreamableFile> {
const user: User | null = await this.usersService.findUser(ftId); const user: User | null = await this.usersService.findUser(ftId)
if (user === null) throw new BadRequestException("User unknown."); if (user === null) throw new BadRequestException('User unknown.')
const filename = user.avatar; const filename = user.avatar
const stream = createReadStream(join(process.cwd(), "avatars/" + filename)); const stream = createReadStream(join(process.cwd(), 'avatars/' + filename))
response.set({ response.set({
"Content-Diposition": `inline; filename='${filename}'`, 'Content-Diposition': `inline; filename='${filename}'`,
"Content-Type": "image/jpg", 'Content-Type': 'image/jpg'
}); })
return new StreamableFile(stream); return new StreamableFile(stream)
} }
@Get(":id") @Get(':id')
async getUserById( async getUserById (
@Param("id", ParseIntPipe) ftId: number @Param('id', ParseIntPipe) ftId: number
): Promise<User | null> { ): Promise<User | null> {
const user = await this.usersService.findUser(ftId); const user = await this.usersService.findUser(ftId)
if (user == null) throw new BadRequestException("User unknown."); if (user == null) throw new BadRequestException('User unknown.')
user.socketKey = ""; user.socketKey = ''
return user; return user
} }
@Post(":id") @Post(':id')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async createById(@Body() payload: UserDto): Promise<void> { async createById (@Body() payload: UserDto): Promise<void> {
const user = await this.usersService.findUser(payload.ftId); const user = await this.usersService.findUser(payload.ftId)
if (user != null) { if (user != null) {
await this.usersService.update(user, payload); await this.usersService.update(user, payload)
} else { } else {
await this.usersService.create(payload); await this.usersService.create(payload)
} }
} }
@Get() @Get()
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getUser(@Profile42() profile: Profile): Promise<User | null> { async getUser (@Profile42() profile: Profile): Promise<User | null> {
return await this.usersService.findUser(profile.id); return await this.usersService.findUser(profile.id)
} }
@Post() @Post()
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async updateUser( async updateUser (
@Body() payload: UserDto, @Body() payload: UserDto,
@Profile42() profile: Profile @Profile42() profile: Profile
): Promise<BadRequestException | User | null> { ): Promise<BadRequestException | User | null> {
const user = await this.usersService.findUser(profile.id); const user = await this.usersService.findUser(profile.id)
if (user == null) throw new BadRequestException("User not found."); if (user == null) throw new BadRequestException('User not found.')
return await this.usersService.update(user, payload); return await this.usersService.update(user, payload)
} }
} }

16
back/volume/src/users/users.module.ts

@ -1,15 +1,15 @@
import { forwardRef, Module } from "@nestjs/common"; import { forwardRef, Module } from '@nestjs/common'
import { TypeOrmModule } from "@nestjs/typeorm"; import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from "./entity/user.entity"; import { User } from './entity/user.entity'
import { UsersController } from "./users.controller"; import { UsersController } from './users.controller'
import { UsersService } from "./users.service"; import { UsersService } from './users.service'
import { PongModule } from "src/pong/pong.module"; import { PongModule } from 'src/pong/pong.module'
import { ChatModule } from "src/chat/chat.module"; import { ChatModule } from 'src/chat/chat.module'
@Module({ @Module({
imports: [forwardRef(() => PongModule), TypeOrmModule.forFeature([User])], imports: [forwardRef(() => PongModule), TypeOrmModule.forFeature([User])],
controllers: [UsersController], controllers: [UsersController],
providers: [UsersService], providers: [UsersService],
exports: [UsersService], exports: [UsersService]
}) })
export class UsersModule {} export class UsersModule {}

210
back/volume/src/users/users.service.ts

@ -1,184 +1,184 @@
import { BadRequestException, Catch, Injectable } from "@nestjs/common"; import { BadRequestException, Catch, Injectable } from '@nestjs/common'
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm'
import { EntityNotFoundError, QueryFailedError, Repository } from "typeorm"; import { EntityNotFoundError, QueryFailedError, Repository } from 'typeorm'
import { Cron } from "@nestjs/schedule"; import { Cron } from '@nestjs/schedule'
import { randomUUID } from "crypto"; import { randomUUID } from 'crypto'
import { type UserDto } from "./dto/user.dto"; import { type UserDto } from './dto/user.dto'
import type Channel from "src/chat/entity/channel.entity"; import type Channel from 'src/chat/entity/channel.entity'
import User from "./entity/user.entity"; import User from './entity/user.entity'
@Injectable() @Injectable()
@Catch(QueryFailedError, EntityNotFoundError) @Catch(QueryFailedError, EntityNotFoundError)
export class UsersService { export class UsersService {
constructor( constructor (
@InjectRepository(User) private readonly usersRepository: Repository<User> @InjectRepository(User) private readonly usersRepository: Repository<User>
) {} ) {}
async save(user: User): Promise<void> { async save (user: User): Promise<void> {
await this.usersRepository.save(user); await this.usersRepository.save(user)
} }
async findUsers(): Promise<User[]> { async findUsers (): Promise<User[]> {
const users = await this.usersRepository.find({}); const users = await this.usersRepository.find({})
users.forEach((usr) => (usr.socketKey = "")); users.forEach((usr) => (usr.socketKey = ''))
return users; return users
} }
async findUserByName(username: string): Promise<User> { async findUserByName (username: string): Promise<User> {
const user = await this.usersRepository.findOne({ const user = await this.usersRepository.findOne({
where: { username }, where: { username },
relations: { results: true }, relations: { results: true }
}); })
if (user == null) throw new BadRequestException("User not found."); if (user == null) throw new BadRequestException('User not found.')
return user; return user
} }
@Cron("0 * * * * *") @Cron('0 * * * * *')
async updateStatus(): Promise<void> { async updateStatus (): Promise<void> {
const users = await this.usersRepository.find({}); const users = await this.usersRepository.find({})
users.forEach((usr) => { users.forEach((usr) => {
if (Date.now() - usr.lastAccess > 60000) { if (Date.now() - usr.lastAccess > 60000) {
usr.isVerified = false; usr.isVerified = false
usr.status = "offline"; usr.status = 'offline'
this.usersRepository.save(usr).catch((err) => { this.usersRepository.save(usr).catch((err) => {
console.log(err); console.log(err)
}); })
} }
}); })
this.getLeaderboard(); this.getLeaderboard()
} }
async findUser(ftId: number): Promise<User | null> { async findUser (ftId: number): Promise<User | null> {
const user = await this.usersRepository.findOneBy({ ftId }); const user = await this.usersRepository.findOneBy({ ftId })
if (user == null) return null; if (user == null) return null
user.lastAccess = Date.now(); user.lastAccess = Date.now()
if (user.status === "offline") user.status = "online"; if (user.status === 'offline') user.status = 'online'
await this.usersRepository.save(user); await this.usersRepository.save(user)
return user; return user
} }
async findOnlineUsers(): Promise<User[]> { async findOnlineUsers (): Promise<User[]> {
const users = await this.usersRepository.find({ const users = await this.usersRepository.find({
where: { status: "online" }, where: { status: 'online' }
}); })
users.forEach((usr) => (usr.socketKey = "")); users.forEach((usr) => (usr.socketKey = ''))
return users; return users
} }
async create(userData: UserDto): Promise<User | null> { async create (userData: UserDto): Promise<User | null> {
try { try {
const newUser = this.usersRepository.create(userData); const newUser = this.usersRepository.create(userData)
newUser.socketKey = randomUUID(); newUser.socketKey = randomUUID()
return await this.usersRepository.save(newUser); return await this.usersRepository.save(newUser)
} catch (err) { } catch (err) {
throw new BadRequestException("User already exists."); throw new BadRequestException('User already exists.')
} }
} }
async findOnlineInChannel(channel: Channel): Promise<User[]> { async findOnlineInChannel (channel: Channel): Promise<User[]> {
return await this.usersRepository return await this.usersRepository
.createQueryBuilder("user") .createQueryBuilder('user')
.where("user.channel = :chan", { chan: channel }) .where('user.channel = :chan', { chan: channel })
.andWhere("user.status := status)", { status: "online" }) .andWhere('user.status := status)', { status: 'online' })
.getMany(); .getMany()
} }
async update(user: User, changes: UserDto): Promise<User | null> { async update (user: User, changes: UserDto): Promise<User | null> {
this.usersRepository.merge(user, changes); this.usersRepository.merge(user, changes)
return await this.usersRepository.save(user); return await this.usersRepository.save(user)
} }
async addAvatar(ftId: number, filename: string): Promise<void> { async addAvatar (ftId: number, filename: string): Promise<void> {
await this.usersRepository.update({ ftId }, { avatar: filename }); await this.usersRepository.update({ ftId }, { avatar: filename })
} }
async getFriends(ftId: number): Promise<User[]> { async getFriends (ftId: number): Promise<User[]> {
const user = await this.usersRepository.findOne({ const user = await this.usersRepository.findOne({
where: { ftId }, where: { ftId },
relations: { friends: true }, relations: { friends: true }
}); })
if (user == null) throw new BadRequestException("User not found."); if (user == null) throw new BadRequestException('User not found.')
user.friends.forEach((friend) => (friend.socketKey = "")); user.friends.forEach((friend) => (friend.socketKey = ''))
return user.friends; return user.friends
} }
async getInvits(ftId: number): Promise<User[]> { async getInvits (ftId: number): Promise<User[]> {
const user = await this.usersRepository.findOne({ const user = await this.usersRepository.findOne({
where: { ftId }, where: { ftId },
relations: { relations: {
followers: true, followers: true
}, }
}); })
if (user == null) throw new BadRequestException("User not found."); if (user == null) throw new BadRequestException('User not found.')
user.followers.forEach((follower) => (follower.socketKey = "")); user.followers.forEach((follower) => (follower.socketKey = ''))
return user.followers; return user.followers
} }
async getLeaderboard(): Promise<User[]> { async getLeaderboard (): Promise<User[]> {
const leaderboard = await this.usersRepository.find({ const leaderboard = await this.usersRepository.find({
order: { order: {
winrate: "ASC", winrate: 'ASC'
}, }
}); })
const ret = leaderboard.filter((user) => user.matchs !== 0); const ret = leaderboard.filter((user) => user.matchs !== 0)
let r = 0; let r = 0
ret.forEach((usr) => { ret.forEach((usr) => {
usr.rank = r++; usr.rank = r++
this.usersRepository.save(usr); this.usersRepository.save(usr)
usr.socketKey = ""; usr.socketKey = ''
}); })
return ret; return ret
} }
async invit(ftId: number, targetFtId: number): Promise<string> { async invit (ftId: number, targetFtId: number): Promise<string> {
const user: User | null = await this.usersRepository.findOne({ const user: User | null = await this.usersRepository.findOne({
where: { ftId }, where: { ftId },
relations: { relations: {
followers: true, followers: true,
friends: true, friends: true
}, }
}); })
if (user === null) throw new BadRequestException("User not found."); if (user === null) throw new BadRequestException('User not found.')
if (user.friends.findIndex((friend) => friend.ftId === targetFtId) !== -1) { if (user.friends.findIndex((friend) => friend.ftId === targetFtId) !== -1) {
return "You are already friends."; return 'You are already friends.'
} }
const target: User | null = await this.usersRepository.findOne({ const target: User | null = await this.usersRepository.findOne({
where: { ftId: targetFtId }, where: { ftId: targetFtId },
relations: { relations: {
followers: true, followers: true,
friends: true, friends: true
}, }
}); })
if (target == null) return "Target not found."; if (target == null) return 'Target not found.'
const id = user.followers.findIndex( const id = user.followers.findIndex(
(follower) => follower.ftId === targetFtId (follower) => follower.ftId === targetFtId
); )
if ( if (
target.followers.findIndex((follower) => follower.ftId === user.ftId) !== target.followers.findIndex((follower) => follower.ftId === user.ftId) !==
-1 -1
) { ) {
return "Invitation already sent."; return 'Invitation already sent.'
} else if ( } else if (
user.followers.findIndex((follower) => follower.ftId === targetFtId) !== user.followers.findIndex((follower) => follower.ftId === targetFtId) !==
-1 -1
) { ) {
user.friends.push(target); user.friends.push(target)
target.friends.push(user); target.friends.push(user)
user.followers.splice(id, 1); user.followers.splice(id, 1)
await this.usersRepository.save(user); await this.usersRepository.save(user)
} else target.followers.push(user); } else target.followers.push(user)
await this.usersRepository.save(target); await this.usersRepository.save(target)
return "OK"; return 'OK'
} }
async findByCode(code: string): Promise<User> { async findByCode (code: string): Promise<User> {
const user = await this.usersRepository.findOneBy({ authToken: code }); const user = await this.usersRepository.findOneBy({ authToken: code })
if (user == null) throw new BadRequestException("User not found"); if (user == null) throw new BadRequestException('User not found')
return user; return user
} }
async turnOnTwoFactorAuthentication(ftId: number): Promise<void> { async turnOnTwoFactorAuthentication (ftId: number): Promise<void> {
await this.usersRepository.update({ ftId }, { twoFA: true }); await this.usersRepository.update({ ftId }, { twoFA: true })
} }
} }

312
front/volume/src/components/Chat.svelte

@ -1,25 +1,23 @@
<script lang="ts" context = "module" > <script lang="ts" context="module">
export interface chatMessagesType { export interface chatMessagesType {
id: number; id: number;
author: string; author: string;
text: string; text: string;
} }
import { createEventDispatcher, onDestroy, onMount } from "svelte"; import { createEventDispatcher, onDestroy, onMount } from "svelte";
import { store, API_URL } from "../Auth"; import { store, API_URL } from "../Auth";
import { io } from "../socket" import { io } from "../socket";
import type { ChannelsType } from "./Channels.svelte"; import type { ChannelsType } from "./Channels.svelte";
import type { User } from "./Profile.svelte"; import type User from "./Profile.svelte";
</script> </script>
< script lang = "ts" > <script lang="ts">
//--------------------------------------------------------------------------------/
let blockedUsers: Array<User> = []; let blockedUsers: Array<User> = [];
let chatMembers: Array<User> = []; let chatMembers: Array<User> = [];
let chatMessages: Array<chatMessagesType> = []; let chatMessages: Array<chatMessagesType> = [];
export let channel: ChannelsType; export let channel: ChannelsType;
let newText = ""; let newText = "";
onMount(async () => { onMount(async () => {
let res = await fetch(API_URL + "/users/" + $store.ftId + "/blocked", { let res = await fetch(API_URL + "/users/" + $store.ftId + "/blocked", {
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",
@ -43,11 +41,11 @@ onMount(async () => {
onDestroy(() => { onDestroy(() => {
io.emit("leaveChannel", channel.id, $store.ftId); io.emit("leaveChannel", channel.id, $store.ftId);
}); });
}); });
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const sendMessage = () => { const sendMessage = () => {
if (newText !== "") { if (newText !== "") {
const newMessage = { const newMessage = {
id: chatMessages.length + 1, id: chatMessages.length + 1,
@ -62,34 +60,34 @@ const sendMessage = () => {
messagesDiv.scrollTop = messagesDiv.scrollHeight; messagesDiv.scrollTop = messagesDiv.scrollHeight;
} }
} }
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let showProfileMenu = false; let showProfileMenu = false;
let selectedUser = null; let selectedUser = null;
function openProfile(username: string) { function openProfile(username: string) {
showProfileMenu = true; showProfileMenu = true;
selectedUser = username; selectedUser = username;
showChatMembers = false; showChatMembers = false;
} }
function closeProfileMenu() { function closeProfileMenu() {
showProfileMenu = false; showProfileMenu = false;
selectedUser = ""; selectedUser = "";
} }
onMount(closeProfileMenu); onMount(closeProfileMenu);
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
let showChatMembers = false; let showChatMembers = false;
function toggleChatMembers() { function toggleChatMembers() {
showChatMembers = !showChatMembers; showChatMembers = !showChatMembers;
} }
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const blockUser = async (username: string) => { const blockUser = async (username: string) => {
const res1 = await fetch(API_URL + "/users/" + username + "/byname", { const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",
@ -106,11 +104,11 @@ const blockUser = async (username: string) => {
} else { } else {
alert("Failed to block user"); alert("Failed to block user");
} }
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const unblockUser = async (username: string) => { const unblockUser = async (username: string) => {
const res1 = await fetch(API_URL + "/users/" + username + "/byname", { const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",
@ -127,11 +125,11 @@ const unblockUser = async (username: string) => {
} else { } else {
alert("Failed to unblock user"); alert("Failed to unblock user");
} }
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const banUser = async (username: string) => { const banUser = async (username: string) => {
const prompt = window.prompt("Enter ban duration in seconds"); const prompt = window.prompt("Enter ban duration in seconds");
const res1 = await fetch(API_URL + "/users/" + username + "/byname", { const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include", credentials: "include",
@ -150,22 +148,22 @@ const banUser = async (username: string) => {
} else { } else {
alert("Failed to ban user"); alert("Failed to ban user");
} }
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const kickUser = async (username: string) => { const kickUser = async (username: string) => {
const res = await fetch(API_URL + "/users/" + username + "/byname", { const res = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",
}); });
const kickedUser = await res.json(); const kickedUser = await res.json();
io.emit("kickUser", channel.id, $store.ftId, kickedUser.ftId); io.emit("kickUser", channel.id, $store.ftId, kickedUser.ftId);
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const muteUser = async (username: string) => { const muteUser = async (username: string) => {
const prompt = window.prompt("Enter mute duration in seconds"); const prompt = window.prompt("Enter mute duration in seconds");
const res1 = await fetch(API_URL + "/users/" + username + "/byname", { const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include", credentials: "include",
@ -183,11 +181,11 @@ const muteUser = async (username: string) => {
} else { } else {
alert("Failed to mute user"); alert("Failed to mute user");
} }
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const adminUser = async (username: string) => { const adminUser = async (username: string) => {
const res1 = await fetch(API_URL + "/users/" + username + "/byname", { const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",
@ -204,11 +202,11 @@ const adminUser = async (username: string) => {
} else { } else {
alert("Failed to admin user"); alert("Failed to admin user");
} }
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const removeAdminUser = async (username: string) => { const removeAdminUser = async (username: string) => {
const res1 = await fetch(API_URL + "/users/" + username + "/byname", { const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",
@ -225,163 +223,161 @@ const removeAdminUser = async (username: string) => {
} else { } else {
alert("Failed to remove admin user"); alert("Failed to remove admin user");
} }
}; };
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
</script> </script>
< div class="overlay" > <div class="overlay">
<div class="chat" on: click | stopPropagation on: keydown | stopPropagation > <div class="chat" on:click|stopPropagation on:keydown|stopPropagation>
<div class="messages" > <div class="messages">
{ #each chatMessages as message } {#each chatMessages as message}
< p class="message" > <p class="message">
{ #if !blockedUsers.filter((user) => user.username == message.author).length } {#if !blockedUsers.filter((user) => user.username == message.author).length}
< span <span
class="message-name" class="message-name"
on: click = {() => openProfile(message.author)} on:click={() => openProfile(message.author)}
on: keydown = {() => openProfile(message.author)} on:keydown={() => openProfile(message.author)}
style = "cursor: pointer;" style="cursor: pointer;"
> >
{ message.author } {message.author}
< /span>: {message.text} </span>: {message.text}
{ {/if}
/if} </p>
< /p> {/each}
{ </div>
/each} {#if showProfileMenu}
< /div>
{ #if showProfileMenu }
<div <div
class="profile-menu" class="profile-menu"
on: click | stopPropagation on:click|stopPropagation
on: keydown | stopPropagation on:keydown|stopPropagation
> >
<ul> <ul>
<li> <li>
<button on: click = {() => dispatch("send-message", selectedUser) <button on:click={() => dispatch("send-message", selectedUser)}>
} Send Message
> Send Message < /button </button>
>
</li> </li>
< li > <li>
<button on: click = {() => dispatch("view-profile", selectedUser) <button on:click={() => dispatch("view-profile", selectedUser)}>
} View Profile
> View Profile < /button </button>
>
</li> </li>
< li > <li>
<button on: click = {() => dispatch("add-friend", selectedUser)} <button on:click={() => dispatch("add-friend", selectedUser)}>
> Add Friend < /button Add Friend
> </button>
</li> </li>
< li > <li>
<button on: click = {() => dispatch("invite-to-game", selectedUser)} <button on:click={() => dispatch("invite-to-game", selectedUser)}>
> Invite to Game < /button Invite to Game
> </button>
</li> </li>
<li> <li>
{ #if!blockedUsers.filter((user) => user.username = selectedUser).length } {#if !blockedUsers.filter((user) => (user.username = selectedUser)).length}
<button on: click = {() => blockUser(selectedUser)}> Block User < /button> <button on:click={() => blockUser(selectedUser)}>
{:else } Block User
<button on: click = {() => unblockUser(selectedUser)}> Unblock User < /button> </button>
{ {:else}
/if} <button on:click={() => unblockUser(selectedUser)}>
< /li> Unblock User
< li > <button on: click = { closeProfileMenu } > Close < /button></li > </button>
{/if}
</li>
<li><button on:click={closeProfileMenu}> Close </button></li>
</ul> </ul>
< /div> </div>
{ {/if}
/if} <form on:submit|preventDefault={sendMessage}>
< form on: submit | preventDefault={ sendMessage }> <input type="text" placeholder="Type a message..." bind:value={newText} />
<input type="text" placeholder = "Type a message..." bind: value = { newText } />
<button> <button>
<img src="img/send.png" alt = "send" /> <img src="img/send.png" alt="send" />
</button> </button>
< /form> </form>
< button <button
on: click | stopPropagation={ toggleChatMembers } on:click|stopPropagation={toggleChatMembers}
on: keydown | stopPropagation > Chat Members < /button on:keydown|stopPropagation
> >
{ #if showChatMembers } Chat Members
< div </button>
{#if showChatMembers}
<div
class="chatMembers" class="chatMembers"
on: click | stopPropagation on:click|stopPropagation
on: keydown | stopPropagation on:keydown|stopPropagation
> >
<div> <div>
<ul> <ul>
{ #each chatMembers as member } {#each chatMembers as member}
< li > <li>
<p> <p>
{ member.username } {member.username}
< button on: click = {() => banUser(member.username) <button on:click={() => banUser(member.username)}>
}> ban < /button> ban
< button on: click = {() => kickUser(member.username) </button>
} <button on:click={() => kickUser(member.username)}>
> kick < /button kick
> </button>
<button on: click = {() => muteUser(member.username)} <button on:click={() => muteUser(member.username)}>
> mute < /button mute
> </button>
<button on: click = {() => adminUser(member.username)} <button on:click={() => adminUser(member.username)}>
> promote < /button promote
> </button>
<button on: click = {() => removeAdminUser(member.username)} <button on:click={() => removeAdminUser(member.username)}>
> demote < /button demote
> </button>
</p> </p>
<p> <p>
----------------------------------------------------------------------------------- -----------------------------------------------------------------------------------
</p> </p>
< /li> </li>
{ {/each}
/each} </ul>
< /ul> </div>
< /div> </div>
< /div> {/if}
{ </div>
/if} </div>
< /div>
< /div>
<style> <style>
.overlay { .overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100 %; width: 100%;
height: 100 %; height: 100%;
background - color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
justify - content: center; justify-content: center;
align - items: center; align-items: center;
} }
.chat { .chat {
background - color: #fff; background-color: #fff;
border: 1px solid #ccc; border: 1px solid #ccc;
border - radius: 5px; border-radius: 5px;
padding: 1rem; padding: 1rem;
width: 300px; width: 300px;
} }
.messages { .messages {
height: 200px; height: 200px;
overflow - y: scroll; overflow-y: scroll;
} }
.chatMembers { .chatMembers {
position: absolute; position: absolute;
background - color: #fff; background-color: #fff;
border: 1px solid #ccc; border: 1px solid #ccc;
border - radius: 5px; border-radius: 5px;
padding: 1rem; padding: 1rem;
max - height: 100px; max-height: 100px;
overflow - y: scroll; overflow-y: scroll;
} }
.chatMembers ul { .chatMembers ul {
list - style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
@ -389,4 +385,4 @@ style = "cursor: pointer;"
.chatMembers button { .chatMembers button {
width: 6rem; width: 6rem;
} }
</style> </style>

Loading…
Cancel
Save