Browse Source

ouai c un peu la merde mais bon

master
Walid Bekkal 2 years ago
parent
commit
8622d6cac7
  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. 74
      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. 282
      back/volume/src/chat/chat.controller.ts
  12. 118
      back/volume/src/chat/chat.gateway.ts
  13. 25
      back/volume/src/chat/chat.module.ts
  14. 206
      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. 9
      back/volume/src/chat/dto/updateUser.dto.ts
  20. 40
      back/volume/src/chat/entity/channel.entity.ts
  21. 18
      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. 30
      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. 32
      back/volume/src/pong/pong.controller.ts
  47. 24
      back/volume/src/pong/pong.gateway.spec.ts
  48. 178
      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. 188
      back/volume/src/users/users.controller.ts
  56. 20
      back/volume/src/users/users.module.ts
  57. 210
      back/volume/src/users/users.service.ts
  58. 19
      docker-compose.yml
  59. 1
      front/volume/.gitignore
  60. 5
      front/volume/src/App.svelte
  61. 15
      front/volume/src/components/Channels.svelte
  62. 689
      front/volume/src/components/Chat.svelte
  63. 5
      front/volume/src/socket.ts

25
.env_sample

@ -1,25 +0,0 @@
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);
} }
} }

74
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);
} }
} }

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

@ -7,171 +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, @Body() target: IdDto, @Param("id") id: number,
@Profile42() profile: Profile) { @Body() target: IdDto,
const channel = await this.channelService.getFullChannel(id) @Profile42() profile: Profile
const user: User | null = await this.usersService.findUser(target.id) ) {
if (user == null) throw new NotFoundException(`User #${target.id} not found`) const channel = await this.channelService.getFullChannel(id);
if (!await this.channelService.isUser(channel.id, +profile.id)) const user: User | null = await this.usersService.findUser(target.id);
throw new BadRequestException('You are not allowed to invite users to this channel') if (user == null) {
if (await this.channelService.isUser(channel.id, target.id)) throw new NotFoundException(`User #${target.id} not found`);
throw new BadRequestException('User is already in this channel') }
if (await this.channelService.isBanned(channel.id, target.id)) if (!(await this.channelService.isUser(channel.id, +profile.id))) {
throw new BadRequestException('User is banned from this channel') throw new BadRequestException(
channel.users.push(user) "You are not allowed to invite users to this channel"
this.channelService.save(channel) );
}
if (await this.channelService.isUser(channel.id, target.id)) {
throw new BadRequestException("User is already in this channel");
}
if (await this.channelService.isBanned(channel.id, target.id)) {
throw new BadRequestException("User is banned from this channel");
}
channel.users.push(user);
this.channelService.save(channel);
} }
@Delete(':id/kick') @Delete(":id/kick")
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async removeUser ( async removeUser(
@Param('id') id: number, @Body() target: IdDto, @Param("id") id: number,
@Profile42() profile: Profile) { @Body() target: IdDto,
const channel = await this.channelService.getFullChannel(id) @Profile42() profile: Profile
if (!await this.channelService.isAdmin(channel.id, +profile.id)) ) {
throw new BadRequestException('You are not allowed to kick users from this channel') const channel = await this.channelService.getFullChannel(id);
if (!await this.channelService.isUser(channel.id, target.id)) if (!(await this.channelService.isAdmin(channel.id, +profile.id))) {
throw new BadRequestException('User is not in this channel') throw new BadRequestException(
if (await this.channelService.isOwner(channel.id, target.id)) "You are not allowed to kick users from this channel"
throw new BadRequestException('You cannot kick the owner of the channel') );
}
if (!(await this.channelService.isUser(channel.id, target.id))) {
throw new BadRequestException("User is not in this channel");
}
if (await this.channelService.isOwner(channel.id, target.id)) {
throw new BadRequestException("You cannot kick the owner of the channel");
}
channel.users = channel.users.filter((usr: User) => { 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 user: User | null = await this.usersService.findUser(target.id) const channel = await this.channelService.getFullChannel(id);
if (user == null) throw new NotFoundException(`User #${target.id} not found`) const user: User | null = await this.usersService.findUser(target.id);
if (!await this.channelService.isOwner(channel.id, +profile.id)) if (user == null) {
throw new BadRequestException('You are not the owner of this channel') throw new NotFoundException(`User #${target.id} not found`);
if (!await this.channelService.isUser(channel.id, target.id)) }
throw new BadRequestException('User is not in this channel') if (!(await this.channelService.isOwner(channel.id, +profile.id))) {
if (await this.channelService.isAdmin(channel.id, target.id)) throw new BadRequestException("You are not the owner of this channel");
throw new BadRequestException('User is already an admin of this channel') }
channel.admins.push(user) if (!(await this.channelService.isUser(channel.id, target.id))) {
this.channelService.save(channel) throw new BadRequestException("User is not in this channel");
}
if (await this.channelService.isAdmin(channel.id, target.id)) {
throw new BadRequestException("User is already an admin of this channel");
}
channel.admins.push(user);
this.channelService.save(channel);
} }
@Delete(':id/admin') @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) ) {
if (!await this.channelService.isOwner(channel.id, +profile.id)) const channel = await this.channelService.getFullChannel(id);
throw new BadRequestException('You are not the owner of this channel') if (!(await this.channelService.isOwner(channel.id, +profile.id))) {
if (!await this.channelService.isAdmin(channel.id, target.id)) throw new BadRequestException("You are not the owner of this channel");
throw new BadRequestException('User is not an admin of this channel') }
if (!(await this.channelService.isAdmin(channel.id, target.id))) {
throw new BadRequestException("User is not an admin of this channel");
}
channel.admins = channel.admins.filter((usr: User) => { 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 user: User | null = await this.usersService.findUser(target.id) const channel = await this.channelService.getFullChannel(id);
if (user == null) throw new NotFoundException(`User #${target.id} not found`) const user: User | null = await this.usersService.findUser(target.id);
if (!await this.channelService.isAdmin(channel.id, +profile.id)) if (user == null) {
throw new BadRequestException('You are not allowed to ban users from this channel') throw new NotFoundException(`User #${target.id} not found`);
if (await this.channelService.isOwner(channel.id, target.id)) }
throw new BadRequestException('You cannot ban the owner of the channel') if (!(await this.channelService.isAdmin(channel.id, +profile.id))) {
if (await this.channelService.isBanned(channel.id, target.id)) throw new BadRequestException(
throw new BadRequestException('User is already banned from this channel') "You are not allowed to ban users from this channel"
channel.banned.push(user) );
this.channelService.save(channel) }
if (await this.channelService.isOwner(channel.id, target.id)) {
throw new BadRequestException("You cannot ban the owner of the channel");
}
if (await this.channelService.isBanned(channel.id, target.id)) {
throw new BadRequestException("User is already banned from this channel");
}
channel.banned.push(user);
this.channelService.save(channel);
} }
@Post(':id/mute') @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 user: User | null = await this.usersService.findUser(mute.data[0]) const channel = await this.channelService.getFullChannel(id);
if (user == null) throw new NotFoundException(`User #${mute.data[0]} not found`) const user: User | null = await this.usersService.findUser(mute.data[0]);
if (!await this.channelService.isAdmin(channel.id, +profile.id)) if (user == null) {
throw new BadRequestException('You are not allowed to mute users from this channel') throw new NotFoundException(`User #${mute.data[0]} not found`);
if (await this.channelService.isOwner(channel.id, mute.data[0])) }
throw new BadRequestException('You cannot mute the owner of the channel') if (!(await this.channelService.isAdmin(channel.id, +profile.id))) {
if (await this.channelService.getMuteDuration(channel.id, mute.data[0]) > 0) throw new BadRequestException(
throw new BadRequestException('User is already muted from this channel') "You are not allowed to mute users from this channel"
let newMute: Array<number> = [mute.data[0], Date.now() + mute.data[1] * 1000] );
channel.muted.push(newMute) }
this.channelService.save(channel) if (await this.channelService.isOwner(channel.id, mute.data[0])) {
throw new BadRequestException("You cannot mute the owner of the channel");
}
if (
(await this.channelService.getMuteDuration(channel.id, mute.data[0])) > 0
) {
throw new BadRequestException("User is already muted from this channel");
}
const newMute: number[] = [mute.data[0], Date.now() + mute.data[1] * 1000];
channel.muted.push(newMute);
this.channelService.save(channel);
} }
@Delete(':id') @Delete(":id")
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async deleteChannel ( async deleteChannel(@Profile42() profile: Profile, @Param("id") id: number) {
@Profile42() profile: Profile, if (!(await this.channelService.isOwner(id, +profile.id))) {
@Param('id') id: number throw new BadRequestException("You are not the owner of this channel");
) { }
if (!await this.channelService.isOwner(id, +profile.id)) await this.channelService.removeChannel(id);
throw new BadRequestException('You are not the owner of this channel')
await this.channelService.removeChannel(id)
return
} }
@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);
} }
} }

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

@ -4,40 +4,39 @@ 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 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,
@InjectRepository(ConnectedUser) @InjectRepository(ConnectedUser)
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)
@ -50,69 +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 (await this.chatService.getMuteDuration(channel.id, message.UserId) > 0) { if (
throw new WsException('You are muted') (await this.chatService.getMuteDuration(channel.id, message.UserId)) > 0
) {
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 (socket: Socket, chan: number, from: number, to: number): Promise<void> { async onKickUser(
const channel = await this.chatService.getChannel(chan) socket: Socket,
chan: number,
from: number,
to: number
): Promise<void> {
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);
} }
} }

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

@ -1,16 +1,16 @@
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: [
@ -20,7 +20,6 @@ import ConnectedUser from './entity/connection.entity'
], ],
controllers: [ChatController], controllers: [ChatController],
providers: [ChatService, ChatGateway, MessageService], providers: [ChatService, ChatGateway, MessageService],
exports: [ChatService] exports: [ChatService],
}) })
export class ChatModule {} export class ChatModule {}

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

@ -1,151 +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() }
newChannel.owner = user const newChannel = new Channel();
newChannel.users = [user] newChannel.owner = user;
newChannel.admins = [user] newChannel.users = [user];
newChannel.name = channel.name newChannel.admins = [user];
newChannel.isPrivate = channel.isPrivate newChannel.name = channel.name;
newChannel.password = channel.password newChannel.isPrivate = channel.isPrivate;
return await this.ChannelRepository.save(newChannel) newChannel.password = channel.password;
return await this.ChannelRepository.save(newChannel);
} }
async updatePassword(id: number, password: string) {
const channel: Channel | null = await this.ChannelRepository.findOneBy({
async updatePassword (id: number, password: string) { id,
let channel: Channel | null = await this.ChannelRepository.findOneBy({id}) });
if (channel === null) { throw new NotFoundException(`Channel #${id} not found`) } if (channel === null) {
channel.password = password throw new NotFoundException(`Channel #${id} not found`);
await this.ChannelRepository.save(channel) }
channel.password = password;
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> {
let channels = await this.ChannelRepository.find({}) const channels = await this.ChannelRepository.find({});
channels.forEach((channel) => { channels.forEach((channel) => {
channel.muted = channel.muted.filter((data) => { return (data[0] - Date.now()) > 0;}); channel.muted = channel.muted.filter((data) => {
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) { throw new NotFoundException(`Channel #${id} not found`) } if (channel == null) {
return channel throw new NotFoundException(`Channel #${id} not found`);
}
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) { throw new NotFoundException(`Channel #${id} not found`) } if (channel == null) {
return channel throw new NotFoundException(`Channel #${id} not found`);
}
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) { throw new NotFoundException(`Channel #${id} not found`) } if (channel == null) {
return channel.owner.ftId === userId throw new NotFoundException(`Channel #${id} not found`);
}
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) { throw new NotFoundException(`Channel #${id} not found`) } if (channel == null) {
return channel.admins.findIndex((user) => user.ftId === userId) != -1 throw new NotFoundException(`Channel #${id} not found`);
}
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) { throw new NotFoundException(`Channel #${id} not found`) } if (channel == null) {
return channel.users.findIndex((user) => user.ftId === userId) != -1 throw new NotFoundException(`Channel #${id} not found`);
}
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) { throw new NotFoundException(`Channel #${id} not found`) } if (channel == null) {
return channel.banned.findIndex((user) => user.ftId === userId) != -1 throw new NotFoundException(`Channel #${id} not found`);
}
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) { throw new NotFoundException(`Channel #${id} not found`) } if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
const mutation: Array<number> | undefined = channel.muted.find((mutation) => mutation[0] === userId) }
if (mutation == null) { return 0 }
return mutation[1] const mutation: number[] | undefined = channel.muted.find(
(mutation) => mutation[0] === userId
);
if (mutation == null) {
return 0;
}
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;
} }

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

@ -1,16 +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: Array<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[][];
} }

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

@ -1,17 +1,23 @@
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm' import {
Column,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
} 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;
} }

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

@ -1,13 +1,13 @@
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 User from 'src/users/entity/user.entity'; import type User from "src/users/entity/user.entity";
import 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 {
@ -15,7 +15,7 @@ export class MessageService {
@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> {
@ -25,17 +25,17 @@ export class MessageService {
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;
} }
} }

32
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();
}) });
}) });

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

@ -1,119 +1,119 @@
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
): void { ): void {
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;

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

@ -11,31 +11,29 @@ 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 { 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 ( constructor(private readonly usersService: UsersService) {}
private readonly usersService: UsersService,
) {}
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
@Post("block/:id") @Post("block/:id")
@ -60,145 +58,151 @@ 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, false);
return;
>>>>>>> ouai c un peu la merde mais bon
} }
callback(null, true) 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);
} }
} }

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

@ -1,17 +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: [ imports: [forwardRef(() => PongModule), TypeOrmModule.forFeature([User])],
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,182 +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) => console.log(err)) this.usersRepository.save(usr).catch((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",
} },
}) });
let 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 });
} }
} }

19
docker-compose.yml

@ -35,22 +35,3 @@ services:
networks: [transcendence] networks: [transcendence]
restart: always restart: always
env_file: .env env_file: .env
pgadmin:
links:
- postgres:postgres
container_name: pgadmin
image: dpage/pgadmin4
ports:
- "8081:80"
volumes:
- /data/pgadmin:/root/.pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: 'usr@usr.com'
PGADMIN_DEFAULT_PASSWORD: 'pw'
GUNICORN_ACCESS_LOGFILE: '/dev/null'
PGADMIN_CONFIG_UPGRADE_CHECK_ENABLED: 'False'
depends_on:
- postgres
networks: [transcendence]
logging:
driver: none

1
front/volume/.gitignore

@ -129,3 +129,4 @@ dist
.yarn/build-state.yml .yarn/build-state.yml
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*

5
front/volume/src/App.svelte

@ -182,7 +182,10 @@
on:keydown={() => on:keydown={() =>
setAppState(APPSTATE.CHANNELS + "#" + selectedChannel.name)} setAppState(APPSTATE.CHANNELS + "#" + selectedChannel.name)}
> >
<Profile username={profileUsername} on:view-history={() => setAppState(APPSTATE.HISTORY_ID)} /> <Profile
username={profileUsername}
on:view-history={() => setAppState(APPSTATE.HISTORY_ID)}
/>
</div> </div>
{/if} {/if}

15
front/volume/src/components/Channels.svelte

@ -8,14 +8,20 @@
} }
import { onMount } from "svelte"; import { onMount } from "svelte";
import { API_URL, store } from "../Auth"; import { API_URL, store } from "../Auth";
import { io } from "../socket";
</script> </script>
<script lang="ts"> <script lang="ts">
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const joinChannel = async (id: number) => {
io.emit("joinChannel", id, $store.ftId);
};
let channels: Array<ChannelsType> = []; let channels: Array<ChannelsType> = [];
onMount(async () => { onMount(async () => {
const res = await fetch(API_URL + "/channels", { const res = await fetch(API_URL + "/channels", {
cors: "include",
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",
}); });
@ -28,6 +34,7 @@
const selectChat = (id: number) => { const selectChat = (id: number) => {
const channel = channels.find((c) => c.id === id); const channel = channels.find((c) => c.id === id);
if (channel) { if (channel) {
joinChannel(id);
onSelectChannel(channel); onSelectChannel(channel);
} }
}; };
@ -118,7 +125,9 @@
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
const changePassword = async (id: number) => { const changePassword = async (id: number) => {
let string = prompt("Enter the new password for this channel (leave empty to remove password) :"); let string = prompt(
"Enter the new password for this channel (leave empty to remove password) :"
);
const response = await fetch(API_URL + "/channels/" + id + "/password", { const response = await fetch(API_URL + "/channels/" + id + "/password", {
credentials: "include", credentials: "include",
method: "POST", method: "POST",
@ -154,7 +163,9 @@
on:keydown={() => removeChannel(_channels.id)}>delete</button on:keydown={() => removeChannel(_channels.id)}>delete</button
> >
<button on:click={() => inviteChannel(_channels.id)}>invite</button> <button on:click={() => inviteChannel(_channels.id)}>invite</button>
<button on:click={() => changePassword(_channels.id)}>Set - Change - Remove Password</button> <button on:click={() => changePassword(_channels.id)}
>Set - Change - Remove Password</button
>
</li>{/each} </li>{/each}
{:else} {:else}
<p>No channels available</p> <p>No channels available</p>

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

@ -1,331 +1,392 @@
<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, onMount } from "svelte"; import { createEventDispatcher, onDestroy, onMount } from "svelte";
import { store, API_URL } from "../Auth"; import { store, API_URL } from "../Auth";
import type { Player } from "./Profile.svelte"; import { io } from "../socket"
interface User { import type { ChannelsType } from "./Channels.svelte";
username: string; import type { User } from "./Profile.svelte";
}
</script> </script>
<script lang="ts"> < script lang = "ts" >
//--------------------------------------------------------------------------------/ //--------------------------------------------------------------------------------/
export let chatMessages: Array<chatMessagesType> = []; let blockedUsers: Array<User> = [];
let newText = ""; let chatMembers: Array<User> = [];
let chatMessages: Array<chatMessagesType> = [];
//--------------------------------------------------------------------------------/ export let channel: ChannelsType;
let newText = "";
const sendMessage = () => { onMount(async () => {
if (newText !== "") { let res = await fetch(API_URL + "/users/" + $store.ftId + "/blocked", {
const newMessage = { credentials: "include",
id: chatMessages.length + 1, mode: "cors",
author: $store.username, });
text: newText, if (res.ok) blockedUsers = await res.json();
};
chatMessages = [...chatMessages.slice(-5 + 1), newMessage]; res = await fetch(API_URL + "/channels/" + channel.id + "/members", {
newText = ""; credentials: "include",
const messagesDiv = document.querySelector(".messages"); mode: "cors",
if (messagesDiv) { });
messagesDiv.scrollTop = messagesDiv.scrollHeight; if (res.ok) chatMembers = await res.json();
}
} io.on("messages", (msgs: Array<chatMessagesType>) => {
}; chatMessages = msgs;
});
//--------------------------------------------------------------------------------/
io.on("newMessage", (msg: chatMessagesType) => {
const dispatch = createEventDispatcher(); chatMessages = [...chatMessages.slice(-5 + 1), msg];
let showProfileMenu = false; });
let selectedUser = null;
function openProfile(username: string) { onDestroy(() => {
showProfileMenu = true; io.emit("leaveChannel", channel.id, $store.ftId);
selectedUser = username; });
showChatMembers = false; });
}
function closeProfileMenu() { //--------------------------------------------------------------------------------/
showProfileMenu = false;
selectedUser = ""; const sendMessage = () => {
} if (newText !== "") {
onMount(closeProfileMenu); const newMessage = {
id: chatMessages.length + 1,
//--------------------------------------------------------------------------------/ author: $store.username,
text: newText,
let showChatMembers = false; };
function toggleChatMembers() { chatMessages = [...chatMessages.slice(-5 + 1)];
showChatMembers = !showChatMembers; io.emit("addMessage", channel.id, $store.ftId, newText);
} newText = "";
let chatMembers: Array<User> = [ const messagesDiv = document.querySelector(".messages");
{ username: "user1" }, if (messagesDiv) {
{ username: "user2" }, messagesDiv.scrollTop = messagesDiv.scrollHeight;
{ username: "user3" }, }
{ username: "user4" }, }
{ username: "user5" }, };
{ username: "user6" },
{ username: "user7" }, //--------------------------------------------------------------------------------/
{ username: "user8" },
{ username: "user9" }, const dispatch = createEventDispatcher();
]; let showProfileMenu = false;
// let chatMembers: Array<Player> = []; let selectedUser = null;
// async function getChatMembers() { function openProfile(username: string) {
// console.log("Getting chat members"); showProfileMenu = true;
// const res = await fetch(API_URL + "/channels/members", { selectedUser = username;
// mode: "cors", showChatMembers = false;
// }); }
// chatMembers = await res.json(); function closeProfileMenu() {
// } showProfileMenu = false;
selectedUser = "";
//--------------------------------------------------------------------------------/ }
onMount(closeProfileMenu);
const blockUser = async (username: string) => {};
//--------------------------------------------------------------------------------/
//--------------------------------------------------------------------------------/
let showChatMembers = false;
const banUser = async (username: string) => { function toggleChatMembers() {
// const prompt = window.prompt("Enter ban duration in seconds"); showChatMembers = !showChatMembers;
// const res1 = await fetch(API_URL + "/users/" + username + "/byname", { }
// mode: "cors",
// }); //--------------------------------------------------------------------------------/
// const data1 = await res1.json();
// const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/ban", { const blockUser = async (username: string) => {
// method: "POST", const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
// mode: "cors", credentials: "include",
// }); mode: "cors",
// const data2 = await res2.json(); });
// if (res2.ok) { const data1 = await res1.json();
// alert("User banned"); const res2 = await fetch(API_URL + "/users/block/" + data1.ftId, {
// } else { credentials: "include",
// alert("Failed to ban user"); method: "POST",
// } mode: "cors",
}; });
const data2 = await res2.json();
//--------------------------------------------------------------------------------/ if (res2.ok) {
alert("User blocked");
const kickUser = async (username: string) => { } else {
// set-up channel joining and kicking alert("Failed to block user");
// const res1 = await fetch(API_URL + "/users/" + username + "/byname", { }
// mode: "cors", };
// });
// const data1 = await res1.json(); //--------------------------------------------------------------------------------/
// const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/kick", {
// method: "POST", const unblockUser = async (username: string) => {
// mode: "cors", const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
// }); credentials: "include",
// const data2 = await res2.json(); mode: "cors",
// if (res2.ok) { });
// alert("User kicked"); const data1 = await res1.json();
// } else { const res2 = await fetch(API_URL + "/users/unblock/" + data1.ftId, {
// alert("Failed to kick user"); credentials: "include",
// } method: "DELETE",
}; mode: "cors",
});
//--------------------------------------------------------------------------------/ const data2 = await res2.json();
if (res2.ok) {
const muteUser = async (username: string) => { alert("User unblocked");
// use minutes prompt to determine mute duration } else {
// const prompt = window.prompt("Enter mute duration in seconds"); alert("Failed to unblock user");
// const res1 = await fetch(API_URL + "/users/" + username + "/byname", { }
// mode: "cors", };
// });
// const data1 = await res1.json(); //--------------------------------------------------------------------------------/
// const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/mute", {
// method: "POST", const banUser = async (username: string) => {
// mode: "cors", const prompt = window.prompt("Enter ban duration in seconds");
// }); const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
// const data2 = await res2.json(); credentials: "include",
// if (res2.ok) { mode: "cors",
// alert("User muted"); });
// } else { const data1 = await res1.json();
// alert("Failed to mute user"); const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/ban", {
// } credentials: "include",
}; method: "POST",
mode: "cors",
//--------------------------------------------------------------------------------/ });
const data2 = await res2.json();
const adminUser = async (username: string) => { if (res2.ok) {
// const res1 = await fetch(API_URL + "/users/" + username + "/byname", { io.emit("kickUser", channel.id, $store.ftId, data1.ftId);
// mode: "cors", alert("User banned");
// }); } else {
// const data1 = await res1.json(); alert("Failed to ban user");
// const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/admin", { }
// method: "POST", };
// mode: "cors",
// }); //--------------------------------------------------------------------------------/
// const data2 = await res2.json();
// if (res2.ok) { const kickUser = async (username: string) => {
// alert("User admined"); const res = await fetch(API_URL + "/users/" + username + "/byname", {
// } else { credentials: "include",
// alert("Failed to admin user"); mode: "cors",
// } });
}; const kickedUser = await res.json();
io.emit("kickUser", channel.id, $store.ftId, kickedUser.ftId);
//--------------------------------------------------------------------------------/ };
const removeAdminUser = async (username: string) => { //--------------------------------------------------------------------------------/
// const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
// mode: "cors", const muteUser = async (username: string) => {
// }); const prompt = window.prompt("Enter mute duration in seconds");
// const data1 = await res1.json(); const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
// const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/admin", { credentials: "include",
// method: "DELETE", mode: "cors",
// mode: "cors", });
// }); const data1 = await res1.json();
// const data2 = await res2.json(); const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/mute", {
// if (res2.ok) { credentials: "include",
// alert("User admin removed"); method: "POST",
// } else { mode: "cors",
// alert("Failed to remove admin user"); });
// } const data2 = await res2.json();
}; if (res2.ok) {
alert("User muted");
//--------------------------------------------------------------------------------/ } else {
alert("Failed to mute user");
}
};
//--------------------------------------------------------------------------------/
const adminUser = async (username: string) => {
const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include",
mode: "cors",
});
const data1 = await res1.json();
const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/admin", {
credentials: "include",
method: "POST",
mode: "cors",
});
const data2 = await res2.json();
if (res2.ok) {
alert("User admined");
} else {
alert("Failed to admin user");
}
};
//--------------------------------------------------------------------------------/
const removeAdminUser = async (username: string) => {
const res1 = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include",
mode: "cors",
});
const data1 = await res1.json();
const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/admin", {
credentials: "include",
method: "DELETE",
mode: "cors",
});
const data2 = await res2.json();
if (res2.ok) {
alert("User admin removed");
} else {
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" >
<span { #if !blockedUsers.filter((user) => user.username == message.author).length }
class="message-name" < span
on:click={() => openProfile(message.author)} class="message-name"
on:keydown={() => openProfile(message.author)} on: click = {() => openProfile(message.author)}
style="cursor: pointer;" on: keydown = {() => openProfile(message.author)}
> style = "cursor: pointer;"
{message.author} >
</span>: {message.text} { message.author }
</p> < /span>: {message.text}
{/each} {
</div> /if}
{#if showProfileMenu} < /p>
<div {
/each}
< /div>
{ #if showProfileMenu }
<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</button }
> > Send Message < /button
</li> >
<li> </li>
<button on:click={() => dispatch("view-profile", selectedUser)} < li >
>View Profile</button <button on: click = {() => dispatch("view-profile", selectedUser)
> }
</li> > View Profile < /button
<li> >
<button on:click={() => dispatch("add-friend", selectedUser)} </li>
>Add Friend</button < li >
> <button on: click = {() => dispatch("add-friend", selectedUser)}
</li> > Add Friend < /button
<li> >
<button on:click={() => dispatch("invite-to-game", selectedUser)} </li>
>Invite to Game</button < li >
> <button on: click = {() => dispatch("invite-to-game", selectedUser)}
</li> > Invite to Game < /button
<li> >
<!-- block only if not blocked --> </li>
<button on:click={() => blockUser(selectedUser)}>Block User</button> <li>
</li> { #if!blockedUsers.filter((user) => user.username = selectedUser).length }
<li><button on:click={closeProfileMenu}>Close</button></li> <button on: click = {() => blockUser(selectedUser)}> Block User < /button>
</ul> {:else }
</div> <button on: click = {() => unblockUser(selectedUser)}> Unblock User < /button>
{/if} {
<form on:submit|preventDefault={sendMessage}> /if}
<input type="text" placeholder="Type a message..." bind:value={newText} /> < /li>
<button> < li > <button on: click = { closeProfileMenu } > Close < /button></li >
<img src="img/send.png" alt="send" /> </ul>
</button> < /div>
</form> {
<button /if}
on:click|stopPropagation={toggleChatMembers} < form on: submit | preventDefault={ sendMessage }>
on:keydown|stopPropagation>Chat Members</button <input type="text" placeholder = "Type a message..." bind: value = { newText } />
> <button>
{#if showChatMembers} <img src="img/send.png" alt = "send" />
<div </button>
class="chatMembers" < /form>
on:click|stopPropagation < button
on:keydown|stopPropagation on: click | stopPropagation={ toggleChatMembers }
> on: keydown | stopPropagation > Chat Members < /button
<div> >
<ul> { #if showChatMembers }
{#each chatMembers as member} < div
<li> class="chatMembers"
<p> on: click | stopPropagation
{member.username} on: keydown | stopPropagation
<button on:click={() => banUser(member.username)}>ban</button> >
<button on:click={() => kickUser(member.username)} <div>
>kick</button <ul>
> { #each chatMembers as member }
<button on:click={() => muteUser(member.username)} < li >
>mute</button <p>
> { member.username }
<button on:click={() => adminUser(member.username)} < button on: click = {() => banUser(member.username)
>promote</button }> ban < /button>
> < button on: click = {() => kickUser(member.username)
<button on:click={() => removeAdminUser(member.username)} }
>demote</button > kick < /button
> >
</p> <button on: click = {() => muteUser(member.username)}
<p> > mute < /button
----------------------------------------------------------------------------------- >
</p> <button on: click = {() => adminUser(member.username)}
</li> > promote < /button
{/each} >
</ul> <button on: click = {() => removeAdminUser(member.username)}
</div> > demote < /button
</div> >
{/if} </p>
</div> <p>
</div> -----------------------------------------------------------------------------------
</p>
< /li>
{
/each}
< /ul>
< /div>
< /div>
{
/if}
< /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;
} }
.chatMembers button { .chatMembers button {
width: 6rem; width: 6rem;
} }
</style> </style>

5
front/volume/src/socket.ts

@ -0,0 +1,5 @@
import ioClient from "socket.io-client";
export const io = ioClient("http://localhost:3001", {
withCredentials: true,
});
Loading…
Cancel
Save