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. 22
      back/volume/src/auth/42-auth.guard.ts
  5. 10
      back/volume/src/auth/42.decorator.ts
  6. 54
      back/volume/src/auth/42.strategy.ts
  7. 58
      back/volume/src/auth/auth.controller.ts
  8. 52
      back/volume/src/auth/auth.module.ts
  9. 46
      back/volume/src/auth/auth.service.ts
  10. 10
      back/volume/src/auth/session.serializer.ts
  11. 260
      back/volume/src/chat/chat.controller.ts
  12. 106
      back/volume/src/chat/chat.gateway.ts
  13. 25
      back/volume/src/chat/chat.module.ts
  14. 174
      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. 38
      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. 54
      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. 96
      back/volume/src/pong/game/Ball.ts
  39. 140
      back/volume/src/pong/game/Game.ts
  40. 78
      back/volume/src/pong/game/Games.ts
  41. 46
      back/volume/src/pong/game/MatchmakingQueue.ts
  42. 24
      back/volume/src/pong/game/Paddle.ts
  43. 42
      back/volume/src/pong/game/Player.ts
  44. 38
      back/volume/src/pong/game/constants.ts
  45. 54
      back/volume/src/pong/game/utils.ts
  46. 24
      back/volume/src/pong/pong.controller.ts
  47. 24
      back/volume/src/pong/pong.gateway.spec.ts
  48. 136
      back/volume/src/pong/pong.gateway.ts
  49. 16
      back/volume/src/pong/pong.module.ts
  50. 84
      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. 156
      back/volume/src/users/users.controller.ts
  56. 20
      back/volume/src/users/users.module.ts
  57. 176
      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. 281
      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 = {
env: {
browser: true,
es2021: true
es2021: true,
},
extends: 'standard-with-typescript',
extends: "standard-with-typescript",
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname
}
}
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
},
};

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

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

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

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

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

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

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

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

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

@ -6,75 +6,75 @@ import {
Res,
Req,
Post,
Body
} from '@nestjs/common'
import { Response, Request } from 'express'
Body,
} from "@nestjs/common";
import { Response, Request } from "express";
import { FtOauthGuard, AuthenticatedGuard } from './42-auth.guard'
import { Profile } from 'passport-42'
import { Profile42 } from './42.decorator'
import { FtOauthGuard, AuthenticatedGuard } from "./42-auth.guard";
import { Profile } from "passport-42";
import { Profile42 } from "./42.decorator";
import { AuthService } from './auth.service'
import { UsersService } from 'src/users/users.service'
import { AuthService } from "./auth.service";
import { UsersService } from "src/users/users.service";
const frontHost =
process.env.HOST !== undefined && process.env.HOST !== ''
process.env.HOST !== undefined && process.env.HOST !== ""
? process.env.HOST
: 'localhost'
: "localhost";
const frontPort =
process.env.PORT !== undefined && process.env.HOST !== ''
process.env.PORT !== undefined && process.env.HOST !== ""
? process.env.PORT
: '80'
: "80";
@Controller('log')
@Controller("log")
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly usersService: UsersService
) {}
@Get('in')
@Get("in")
@UseGuards(FtOauthGuard)
ftAuth(): void {}
@Get('inReturn')
@Get("inReturn")
@UseGuards(FtOauthGuard)
@Redirect(`http://${frontHost}:${frontPort}`)
ftAuthCallback(
@Res({ passthrough: true }) response: Response,
@Req() request: Request
): any {
console.log('cookie:', request.cookies['connect.sid'])
response.cookie('connect.sid', request.cookies['connect.sid'])
console.log("cookie:", request.cookies["connect.sid"]);
response.cookie("connect.sid", request.cookies["connect.sid"]);
}
@Get('/verify')
@Get("/verify")
@UseGuards(AuthenticatedGuard)
@Redirect(`http://${frontHost}:${frontPort}`)
async VerifyEmail(@Profile42() profile: Profile): Promise<void> {
const ftId: number = profile.id
const user = await this.usersService.findUser(ftId)
if (user == null) throw new Error('User not found')
await this.authService.sendConfirmationEmail(user)
const ftId: number = profile.id;
const user = await this.usersService.findUser(ftId);
if (user == null) throw new Error("User not found");
await this.authService.sendConfirmationEmail(user);
}
@Post('/verify')
@Post("/verify")
@Redirect(`http://${frontHost}:${frontPort}`)
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)
profile(@Profile42() user: Profile): any {
return { user }
return { user };
}
@Get('out')
@Get("out")
@Redirect(`http://${frontHost}:${frontPort}`)
logOut(@Req() req: Request): any {
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 { UsersModule } from 'src/users/users.module'
import { PassportModule } from '@nestjs/passport'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { AuthController } from './auth.controller'
import { FtStrategy } from './42.strategy'
import { SessionSerializer } from './session.serializer'
import { JwtModule } from '@nestjs/jwt'
import { MailerModule } from '@nestjs-modules/mailer'
import { AuthService } from './auth.service'
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'
import { Module } from "@nestjs/common";
import { UsersModule } from "src/users/users.module";
import { PassportModule } from "@nestjs/passport";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { AuthController } from "./auth.controller";
import { FtStrategy } from "./42.strategy";
import { SessionSerializer } from "./session.serializer";
import { JwtModule } from "@nestjs/jwt";
import { MailerModule } from "@nestjs-modules/mailer";
import { AuthService } from "./auth.service";
import { HandlebarsAdapter } from "@nestjs-modules/mailer/dist/adapters/handlebars.adapter";
const mailUser =
process.env.MAIL_USER !== null && process.env.MAIL_USER !== ''
process.env.MAIL_USER !== null && process.env.MAIL_USER !== ""
? process.env.MAIL_USER
: ''
: "";
const mailPass =
process.env.MAIL_PASSWORD !== null && process.env.MAIL_PASSWORD !== ''
process.env.MAIL_PASSWORD !== null && process.env.MAIL_PASSWORD !== ""
? process.env.MAIL_PASSWORD
: ''
: "";
@Module({
imports: [
@ -26,29 +26,29 @@ const mailPass =
ConfigModule.forRoot(),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '60s' }
signOptions: { expiresIn: "60s" },
}),
MailerModule.forRoot({
transport: {
service: 'gmail',
service: "gmail",
auth: {
user: mailUser,
pass: mailPass
}
pass: mailPass,
},
},
template: {
dir: 'src/auth/mails',
dir: "src/auth/mails",
adapter: new HandlebarsAdapter(),
options: {
strict: true
}
strict: true,
},
},
defaults: {
from: '"No Reply" vaganiwast@gmail.com'
}
})
from: '"No Reply" vaganiwast@gmail.com',
},
}),
],
providers: [ConfigService, FtStrategy, SessionSerializer, AuthService],
controllers: [AuthController]
controllers: [AuthController],
})
export class AuthModule {}

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

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

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

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

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

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

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

@ -4,29 +4,29 @@ import {
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsException
} from '@nestjs/websockets'
import { Socket, Server } from 'socket.io'
WsException,
} from "@nestjs/websockets";
import { Socket, Server } from "socket.io";
// import { User } from 'users/user.entity';
import { UsersService } from 'src/users/users.service'
import { BadRequestException } from '@nestjs/common'
import { ChatService } from './chat.service'
import Message from './entity/message.entity'
import * as bcrypt from 'bcrypt'
import { MessageService } from './message.service'
import { type User } from 'src/users/entity/user.entity'
import { CreateMessageDto } from './dto/create-message.dto'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import ConnectedUser from './entity/connection.entity'
import { ConnectionDto } from './dto/connection.dto'
import { UsersService } from "src/users/users.service";
import { BadRequestException } from "@nestjs/common";
import { ChatService } from "./chat.service";
import type Message from "./entity/message.entity";
import * as bcrypt from "bcrypt";
import { MessageService } from "./message.service";
import { type User } from "src/users/entity/user.entity";
import { CreateMessageDto } from "./dto/create-message.dto";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import ConnectedUser from "./entity/connection.entity";
import { ConnectionDto } from "./dto/connection.dto";
@WebSocketGateway({
cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ }
cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ },
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server
server: Server;
constructor(
private readonly userService: UsersService,
@ -34,8 +34,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly chatService: ChatService,
@InjectRepository(ConnectedUser)
private readonly connectedUserRepository: Repository<ConnectedUser>
) {
}
) {}
async handleConnection(socket: Socket): Promise<void> {
// console.log(socket.handshake.headers)
@ -51,68 +50,75 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
}
handleDisconnect(socket: Socket): void {
socket.disconnect()
socket.disconnect();
}
@SubscribeMessage('joinChannel')
@SubscribeMessage("joinChannel")
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) {
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 (
channel.users.find((e) => e.id === user.id) == null &&
channel.password !== ''
channel.password !== ""
) {
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()
conUser.user = user
conUser.channel = channel
conUser.socket = socket.id
await this.connectedUserRepository.save(conUser)
const conUser = new ConnectedUser();
conUser.user = user;
conUser.channel = channel;
conUser.socket = socket.id;
await this.connectedUserRepository.save(conUser);
}
const messages = await this.messageService.findMessagesInChannelForUser(
channel,
user
)
this.server.to(socket.id).emit('messages', messages)
await socket.join(channel.name)
);
this.server.to(socket.id).emit("messages", messages);
await socket.join(channel.name);
}
@SubscribeMessage('leaveChannel')
@SubscribeMessage("leaveChannel")
async onLeaveChannel(socket: Socket): Promise<void> {
const id = socket.id as any
await this.connectedUserRepository.delete({ socket: id })
socket.disconnect()
const id = socket.id as any;
await this.connectedUserRepository.delete({ socket: id });
socket.disconnect();
}
@SubscribeMessage('addMessage')
@SubscribeMessage("addMessage")
async onAddMessage(socket: Socket, message: CreateMessageDto): Promise<void> {
const channel = await this.chatService.getChannel(message.ChannelId)
if (await this.chatService.getMuteDuration(channel.id, message.UserId) > 0) {
throw new WsException('You are muted')
const channel = await this.chatService.getChannel(message.ChannelId);
if (
(await this.chatService.getMuteDuration(channel.id, message.UserId)) > 0
) {
throw new WsException("You are muted");
}
const createdMessage: Message = await this.messageService.createMessage(
message
)
socket.in(channel.name).emit('newMessage', createdMessage)
);
socket.in(channel.name).emit("newMessage", createdMessage);
}
@SubscribeMessage('kickUser')
async onKickUser (socket: Socket, chan: number, from: number, to: number): Promise<void> {
const channel = await this.chatService.getChannel(chan)
@SubscribeMessage("kickUser")
async onKickUser(
socket: Socket,
chan: number,
from: number,
to: number
): Promise<void> {
const channel = await this.chatService.getChannel(chan);
if (
channel.owner.id !== from ||
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 { TypeOrmModule } from '@nestjs/typeorm'
import { forwardRef, Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AuthModule } from 'src/auth/auth.module'
import { UsersModule } from 'src/users/users.module'
import { ChatGateway } from './chat.gateway'
import { ChatController } from './chat.controller'
import { ChatService } from './chat.service'
import { MessageService } from './message.service'
import { AuthModule } from "src/auth/auth.module";
import { UsersModule } from "src/users/users.module";
import { ChatGateway } from "./chat.gateway";
import { ChatController } from "./chat.controller";
import { ChatService } from "./chat.service";
import { MessageService } from "./message.service";
import Channel from './entity/channel.entity'
import Message from './entity/message.entity'
import ConnectedUser from './entity/connection.entity'
import Channel from "./entity/channel.entity";
import Message from "./entity/message.entity";
import ConnectedUser from "./entity/connection.entity";
@Module({
imports: [
@ -20,7 +20,6 @@ import ConnectedUser from './entity/connection.entity'
],
controllers: [ChatController],
providers: [ChatService, ChatGateway, MessageService],
exports: [ChatService]
exports: [ChatService],
})
export class ChatModule {}

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

@ -1,13 +1,13 @@
import { Inject, Injectable, NotFoundException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { type CreateChannelDto } from './dto/create-channel.dto'
import { UsersService } from 'src/users/users.service'
import { type CreateChannelDto } from "./dto/create-channel.dto";
import { UsersService } from "src/users/users.service";
import type User from 'src/users/entity/user.entity'
import Channel from './entity/channel.entity'
import { Cron } from '@nestjs/schedule'
import type User from "src/users/entity/user.entity";
import Channel from "./entity/channel.entity";
import { Cron } from "@nestjs/schedule";
@Injectable()
export class ChatService {
@ -18,134 +18,156 @@ export class ChatService {
) {}
async createChannel(channel: CreateChannelDto): Promise<Channel> {
const user: User | null = await this.usersService.findUser(channel.owner)
if (user == null)
throw new NotFoundException(`User #${channel.owner} not found`)
const newChannel = new Channel()
newChannel.owner = user
newChannel.users = [user]
newChannel.admins = [user]
newChannel.name = channel.name
newChannel.isPrivate = channel.isPrivate
newChannel.password = channel.password
return await this.ChannelRepository.save(newChannel)
const user: User | null = await this.usersService.findUser(channel.owner);
if (user == null) {
throw new NotFoundException(`User #${channel.owner} not found`);
}
const newChannel = new Channel();
newChannel.owner = user;
newChannel.users = [user];
newChannel.admins = [user];
newChannel.name = channel.name;
newChannel.isPrivate = channel.isPrivate;
newChannel.password = channel.password;
return await this.ChannelRepository.save(newChannel);
}
async updatePassword(id: number, password: string) {
let channel: Channel | null = await this.ChannelRepository.findOneBy({id})
if (channel === null) { throw new NotFoundException(`Channel #${id} not found`) }
channel.password = password
await this.ChannelRepository.save(channel)
const channel: Channel | null = await this.ChannelRepository.findOneBy({
id,
});
if (channel === null) {
throw new NotFoundException(`Channel #${id} not found`);
}
channel.password = password;
await this.ChannelRepository.save(channel);
}
async getChannelsForUser(ftId: number): Promise<Channel[]> {
let rooms: Channel[] = []
let rooms: Channel[] = [];
rooms = [
...(await this.ChannelRepository.createQueryBuilder('room')
.where('room.isPrivate = false')
.getMany())
]
...(await this.ChannelRepository.createQueryBuilder("room")
.where("room.isPrivate = false")
.getMany()),
];
rooms = [
...rooms,
...(await this.ChannelRepository.createQueryBuilder('room')
.innerJoin('room.users', 'users')
.where('room.isPrivate = true')
.andWhere('users.ftId = :ftId', { ftId })
.getMany())
]
return rooms
...(await this.ChannelRepository.createQueryBuilder("room")
.innerJoin("room.users", "users")
.where("room.isPrivate = true")
.andWhere("users.ftId = :ftId", { ftId })
.getMany()),
];
return rooms;
}
@Cron('*/6 * * * * *')
@Cron("*/6 * * * * *")
async updateMutelists(): Promise<void> {
let channels = await this.ChannelRepository.find({})
const channels = await this.ChannelRepository.find({});
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);
})
});
}
async addUserToChannel(channel: Channel, user: User): Promise<Channel> {
channel.owner = user
return await this.ChannelRepository.save(channel)
channel.owner = user;
return await this.ChannelRepository.save(channel);
}
async getChannel(id: number): Promise<Channel> {
const channel = await this.ChannelRepository.findOneBy({ id })
if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) }
return channel
const channel = await this.ChannelRepository.findOneBy({ id });
if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
}
return channel;
}
async getFullChannel(id: number): Promise<Channel> {
const channel = await this.ChannelRepository.findOne({
where: { id },
relations: ['users', 'admins', 'banned', 'muted', 'owner']
})
if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) }
return channel
relations: ["users", "admins", "banned", "muted", "owner"],
});
if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
}
return channel;
}
async update(channel: Channel) {
await this.ChannelRepository.update(channel.id, channel)
await this.ChannelRepository.update(channel.id, channel);
}
async save(channel: Channel) {
await this.ChannelRepository.save(channel)
await this.ChannelRepository.save(channel);
}
async removeChannel(channelId: number) {
await this.ChannelRepository.delete(channelId)
await this.ChannelRepository.delete(channelId);
}
async isOwner(id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({
where: { id },
relations: { owner: true }
})
if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) }
return channel.owner.ftId === userId
relations: { owner: true },
});
if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
}
return channel.owner.ftId === userId;
}
async isAdmin(id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({
where: { id },
relations: { admins: true }
})
if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) }
return channel.admins.findIndex((user) => user.ftId === userId) != -1
relations: { admins: true },
});
if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
}
return channel.admins.findIndex((user) => user.ftId === userId) != -1;
}
async isUser(id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({
where: { id },
relations: { users: true }
})
if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) }
return channel.users.findIndex((user) => user.ftId === userId) != -1
relations: { users: true },
});
if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
}
return channel.users.findIndex((user) => user.ftId === userId) != -1;
}
async isBanned(id: number, userId: number): Promise<boolean> {
const channel = await this.ChannelRepository.findOne({
where: { id },
relations: { banned: true }
})
if (channel == null) { throw new NotFoundException(`Channel #${id} not found`) }
return channel.banned.findIndex((user) => user.ftId === userId) != -1
relations: { banned: true },
});
if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
}
return channel.banned.findIndex((user) => user.ftId === userId) != -1;
}
async getMuteDuration(id: number, userId: number): Promise<number> {
const channel = await this.ChannelRepository.findOne({
where: { id },
relations: { muted: true }
})
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]
relations: { muted: true },
});
if (channel == null) {
throw new NotFoundException(`Channel #${id} not found`);
}
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 {
@IsNumber()
UserId: number
UserId: number;
@IsNumber()
ChannelId: number
ChannelId: number;
@IsString()
@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 {
IsPositive,
IsAlpha,
IsString,
IsOptional,
IsNumber,
IsBoolean
} from 'class-validator'
IsBoolean,
} from "class-validator";
export class CreateChannelDto {
@IsOptional()
@IsPositive()
id: number
id: number;
@IsString()
name: string
name: string;
@IsNumber()
owner: number
owner: number;
@IsOptional()
password: string
password: string;
@IsBoolean()
@Transform(({ value }) => value === 'true')
isPrivate: boolean
@Transform(({ value }) => value === "true")
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 {
@IsString()
text: string
text: string;
@IsNumber()
UserId: number
UserId: number;
@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 { CreateChannelDto } from './create-channel.dto'
import { IsNumber, IsOptional, IsString } from 'class-validator'
import { PartialType } from "@nestjs/mapped-types";
import { CreateChannelDto } from "./create-channel.dto";
import { IsNumber, IsOptional, IsString } from "class-validator";
export class UpdateChannelDto extends PartialType(CreateChannelDto) {
id: number
id: number;
@IsOptional()
@IsNumber()
users: [number]
users: [number];
@IsOptional()
@IsNumber()
messages: [number]
messages: [number];
@IsOptional()
@IsNumber()
owners: [number] // user id
owners: [number]; // user id
@IsOptional()
@IsNumber()
banned: [number] // user id
banned: [number]; // user id
@IsOptional()
@IsNumber()
muted: [number] // user id
muted: [number]; // user id
@IsString()
@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 {
@IsNumber()
id: number
id: number;
}
export class PasswordDto {
@IsString()
password: string
password: string;
}
export class MuteDto {
data: Array<number>
data: number[];
}

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

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

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

@ -5,27 +5,27 @@ import {
JoinColumn,
JoinTable,
ManyToOne,
PrimaryGeneratedColumn
} from 'typeorm'
import User from 'src/users/entity/user.entity'
import Channel from './channel.entity'
PrimaryGeneratedColumn,
} from "typeorm";
import User from "src/users/entity/user.entity";
import Channel from "./channel.entity";
@Entity()
export default class Message {
@PrimaryGeneratedColumn()
id: number
id: number;
@Column()
text: string
text: string;
@ManyToOne(() => User)
@JoinColumn()
author: User
author: User;
@ManyToOne(() => Channel, (channel) => channel.messages, { cascade: true })
@JoinTable()
channel: Channel
channel: Channel;
@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 { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ChatService } from './chat.service';
import { UsersService } from 'src/users/users.service';
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { ChatService } from "./chat.service";
import { UsersService } from "src/users/users.service";
import { type CreateMessageDto } from './dto/create-message.dto';
import User from 'src/users/entity/user.entity';
import Channel from './entity/channel.entity';
import Message from './entity/message.entity';
import { type CreateMessageDto } from "./dto/create-message.dto";
import type User from "src/users/entity/user.entity";
import type Channel from "./entity/channel.entity";
import Message from "./entity/message.entity";
@Injectable()
export class MessageService {
@ -15,7 +15,7 @@ export class MessageService {
@InjectRepository(Message)
private readonly MessageRepository: Repository<Message>,
private readonly channelService: ChatService,
private readonly usersService: UsersService,
private readonly usersService: UsersService
) {}
async createMessage(message: CreateMessageDto): Promise<Message> {
@ -25,17 +25,17 @@ export class MessageService {
msg.author = (await this.usersService.findUser(message.UserId)) as User;
msg.channel.messages.push(msg);
return await this.MessageRepository.save(
this.MessageRepository.create(msg),
this.MessageRepository.create(msg)
);
}
async findMessagesInChannelForUser(
channel: Channel,
user: User,
user: User
): Promise<Message[]> {
return await this.MessageRepository.createQueryBuilder('message')
.where('message.channel = :chan', { chan: channel })
.andWhere('message.author NOT IN (:...blocked)', {
return await this.MessageRepository.createQueryBuilder("message")
.where("message.channel = :chan", { chan: channel })
.andWhere("message.author NOT IN (:...blocked)", {
blocked: user.blocked,
})
.getMany();

54
back/volume/src/main.ts

@ -1,45 +1,45 @@
import { InternalServerErrorException, Logger } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import * as session from 'express-session'
import * as passport from 'passport'
import { type NestExpressApplication } from '@nestjs/platform-express'
import * as cookieParser from 'cookie-parser'
import { IoAdapter } from '@nestjs/platform-socket.io'
import { InternalServerErrorException, Logger } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import * as session from "express-session";
import * as passport from "passport";
import { type NestExpressApplication } from "@nestjs/platform-express";
import * as cookieParser from "cookie-parser";
import { IoAdapter } from "@nestjs/platform-socket.io";
async function bootstrap(): Promise<void> {
const logger = new Logger()
const app = await NestFactory.create<NestExpressApplication>(AppModule)
const logger = new Logger();
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const port =
process.env.BACK_PORT !== undefined && process.env.BACK_PORT !== ''
process.env.BACK_PORT !== undefined && process.env.BACK_PORT !== ""
? +process.env.BACK_PORT
: 3001
: 3001;
const cors = {
origin: /^(http|ws):\/\/localhost(:\d+)?$/,
methods: 'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS',
methods: "GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS",
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: true,
allowedHeaders: ['Accept', 'Content-Type', 'Authorization']
}
allowedHeaders: ["Accept", "Content-Type", "Authorization"],
};
app.use(
session({
resave: false,
saveUninitialized: false,
secret:
process.env.JWT_SECRET !== undefined && process.env.JWT_SECRET !== ''
process.env.JWT_SECRET !== undefined && process.env.JWT_SECRET !== ""
? process.env.JWT_SECRET
: 'secret'
: "secret",
})
)
app.use(cookieParser())
app.use(passport.initialize())
app.use(passport.session())
app.enableCors(cors)
app.useWebSocketAdapter(new IoAdapter(app))
await app.listen(port)
logger.log(`Application listening on port ${port}`)
);
app.use(cookieParser());
app.use(passport.initialize());
app.use(passport.session());
app.enableCors(cors);
app.useWebSocketAdapter(new IoAdapter(app));
await app.listen(port);
logger.log(`Application listening on port ${port}`);
}
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 {
ArrayMaxSize,
ArrayMinSize,
@ -7,32 +7,32 @@ import {
IsString,
Max,
Min,
ValidateNested
} from 'class-validator'
ValidateNested,
} from "class-validator";
import {
DEFAULT_BALL_INITIAL_SPEED,
DEFAULT_MAX_BALL_SPEED
} from '../game/constants'
import { MapDtoValidated } from './MapDtoValidated'
DEFAULT_MAX_BALL_SPEED,
} from "../game/constants";
import { MapDtoValidated } from "./MapDtoValidated";
export class GameCreationDtoValidated {
@IsString({ each: true })
@ArrayMaxSize(2)
@ArrayMinSize(2)
playerNames!: string[]
playerNames!: string[];
@IsNotEmptyObject()
@ValidateNested()
@Type(() => MapDtoValidated)
map!: MapDtoValidated
map!: MapDtoValidated;
@IsNumber()
@Min(DEFAULT_BALL_INITIAL_SPEED.x)
@Max(DEFAULT_MAX_BALL_SPEED.x)
initialBallSpeedX!: number
initialBallSpeedX!: number;
@IsNumber()
@Min(DEFAULT_BALL_INITIAL_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 {
mapSize!: Point
yourPaddleIndex!: number
gameId!: string
walls!: Rect[]
paddleSize!: Point
ballSize!: Point
winScore!: number
ranked!: boolean
mapSize!: Point;
yourPaddleIndex!: number;
gameId!: string;
walls!: Rect[];
paddleSize!: Point;
ballSize!: Point;
winScore!: number;
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 {
paddlesPositions!: Point[]
ballSpeed!: Point
ballPosition!: Point
scores!: number[]
paddlesPositions!: Point[];
ballSpeed!: Point;
ballPosition!: Point;
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 {
ArrayMaxSize,
IsArray,
IsDefined,
IsObject,
ValidateNested
} from 'class-validator'
import { PointDtoValidated } from './PointDtoValidated'
import { RectDtoValidated } from './RectDtoValidated'
ValidateNested,
} from "class-validator";
import { PointDtoValidated } from "./PointDtoValidated";
import { RectDtoValidated } from "./RectDtoValidated";
export class MapDtoValidated {
@IsObject()
@IsDefined()
@Type(() => PointDtoValidated)
size!: PointDtoValidated
size!: PointDtoValidated;
@IsArray()
@ArrayMaxSize(5)
@ValidateNested({ each: true })
@Type(() => RectDtoValidated)
walls!: RectDtoValidated[]
walls!: RectDtoValidated[];
}

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

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

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

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

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

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

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

@ -1,3 +1,3 @@
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 { StringDto } from './StringDto'
import { IsString } from "class-validator";
import { StringDto } from "./StringDto";
export class StringDtoValidated extends StringDto {
@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 {
@IsString()
username!: string
username!: string;
@IsString()
avatar!: string
avatar!: string;
@IsString()
status!: string
status!: string;
}

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

@ -3,25 +3,25 @@ import {
PrimaryGeneratedColumn,
Column,
ManyToMany,
CreateDateColumn
} from 'typeorm'
CreateDateColumn,
} from "typeorm";
import User from 'src/users/entity/user.entity'
import User from "src/users/entity/user.entity";
@Entity()
export default class Result {
@PrimaryGeneratedColumn()
id: number
id: number;
@Column({ default: false })
ranked: boolean
ranked: boolean;
@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 })
public score: number[]
@Column("text", { array: true })
public score: number[];
@CreateDateColumn()
date: Date
date: Date;
}

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

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

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

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

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

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

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

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

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

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

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

@ -1,16 +1,16 @@
import { type Socket } from 'socket.io'
import { Paddle } from './Paddle'
import { type Point } from './utils'
import { type Socket } from "socket.io";
import { Paddle } from "./Paddle";
import { type Point } from "./utils";
export class Player {
socket: Socket
uuid: string
name: string
ready: boolean
paddle: Paddle
paddleCoords: Point
mapSize: Point
score: number
socket: Socket;
uuid: string;
name: string;
ready: boolean;
paddle: Paddle;
paddleCoords: Point;
mapSize: Point;
score: number;
constructor(
socket: Socket,
@ -19,18 +19,18 @@ export class Player {
paddleCoords: Point,
mapSize: Point
) {
this.socket = socket
this.uuid = uuid
this.name = name
this.ready = false
this.paddle = new Paddle(paddleCoords, mapSize)
this.paddleCoords = paddleCoords
this.mapSize = mapSize
this.score = 0
this.socket = socket;
this.uuid = uuid;
this.name = name;
this.ready = false;
this.paddle = new Paddle(paddleCoords, mapSize);
this.paddleCoords = paddleCoords;
this.mapSize = mapSize;
this.score = 0;
}
newGame(): void {
this.score = 0
this.paddle = new Paddle(this.paddleCoords, this.mapSize)
this.score = 0;
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 = {
START_GAME: 'START_GAME',
READY: 'READY',
GAME_TICK: 'GAME_TICK',
PLAYER_MOVE: 'PLAYER_MOVE',
GET_GAME_INFO: 'GET_GAME_INFO',
CREATE_GAME: 'CREATE_GAME',
REGISTER_PLAYER: 'REGISTER_PLAYER',
MATCHMAKING: 'MATCHMAKING',
LEAVE_GAME: 'LEAVE_GAME'
}
START_GAME: "START_GAME",
READY: "READY",
GAME_TICK: "GAME_TICK",
PLAYER_MOVE: "PLAYER_MOVE",
GET_GAME_INFO: "GET_GAME_INFO",
CREATE_GAME: "CREATE_GAME",
REGISTER_PLAYER: "REGISTER_PLAYER",
MATCHMAKING: "MATCHMAKING",
LEAVE_GAME: "LEAVE_GAME",
};
export const DEFAULT_MAP_SIZE = new Point(500, 400)
export const DEFAULT_PADDLE_SIZE = new Point(30, 50)
export const DEFAULT_BALL_SIZE = new Point(10, 10)
export const DEFAULT_BALL_INITIAL_SPEED = new Point(10, 2)
export const DEFAULT_MAX_BALL_SPEED = new Point(30, 20)
export const DEFAULT_BALL_SPEED_INCREMENT = new Point(0.05, 0)
export const DEFAULT_WIN_SCORE = 5
export const GAME_TICKS = 30
export const DEFAULT_MAP_SIZE = new Point(500, 400);
export const DEFAULT_PADDLE_SIZE = new Point(30, 50);
export const DEFAULT_BALL_SIZE = new Point(10, 10);
export const DEFAULT_BALL_INITIAL_SPEED = new Point(10, 2);
export const DEFAULT_MAX_BALL_SPEED = new Point(30, 20);
export const DEFAULT_BALL_SPEED_INCREMENT = new Point(0.05, 0);
export const DEFAULT_WIN_SCORE = 5;
export const GAME_TICKS = 30;

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

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

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

@ -3,31 +3,31 @@ import {
Get,
Param,
ParseIntPipe,
UseGuards
} from '@nestjs/common'
import { Paginate, type Paginated, PaginateQuery } from 'nestjs-paginate'
import { AuthenticatedGuard } from 'src/auth/42-auth.guard'
import type Result from './entity/result.entity'
import { PongService } from './pong.service'
UseGuards,
} from "@nestjs/common";
import { Paginate, type Paginated, PaginateQuery } from "nestjs-paginate";
import { AuthenticatedGuard } from "src/auth/42-auth.guard";
import type Result from "./entity/result.entity";
import { PongService } from "./pong.service";
@Controller('results')
@Controller("results")
export class PongController {
constructor(private readonly pongService: PongService) {}
@Get('global')
@Get("global")
@UseGuards(AuthenticatedGuard)
async getGlobalHistory(
@Paginate() query: PaginateQuery
): Promise<Paginated<Result>> {
return await this.pongService.getHistory(query, 0)
return await this.pongService.getHistory(query, 0);
}
@Get(':id')
@Get(":id")
@UseGuards(AuthenticatedGuard)
async getHistoryById(
@Param('id', ParseIntPipe) id: number,
@Param("id", ParseIntPipe) id: number,
@Paginate() query: PaginateQuery
): 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 { PongGateway } from './pong.gateway'
import { Test, type TestingModule } from "@nestjs/testing";
import { PongGateway } from "./pong.gateway";
describe('PongGateway', () => {
let gateway: PongGateway
describe("PongGateway", () => {
let gateway: PongGateway;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PongGateway]
}).compile()
providers: [PongGateway],
}).compile();
gateway = module.get<PongGateway>(PongGateway)
})
gateway = module.get<PongGateway>(PongGateway);
});
it('should be defined', () => {
expect(gateway).toBeDefined()
})
})
it("should be defined", () => {
expect(gateway).toBeDefined();
});
});

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

@ -1,28 +1,28 @@
import { UsePipes, ValidationPipe } from '@nestjs/common'
import { Socket } from 'socket.io'
import { UsePipes, ValidationPipe } from "@nestjs/common";
import { Socket } from "socket.io";
import {
ConnectedSocket,
MessageBody,
type OnGatewayConnection,
type OnGatewayDisconnect,
SubscribeMessage,
WebSocketGateway
} from '@nestjs/websockets'
import { Games } from './game/Games'
import { GAME_EVENTS } from './game/constants'
import { GameCreationDtoValidated } from './dtos/GameCreationDtoValidated'
import { type Game } from './game/Game'
import { plainToClass } from 'class-transformer'
import { PointDtoValidated } from './dtos/PointDtoValidated'
import { StringDtoValidated } from './dtos/StringDtoValidated'
import { MatchmakingQueue } from './game/MatchmakingQueue'
import { MatchmakingDtoValidated } from './dtos/MatchmakingDtoValidated'
import { PongService } from './pong.service'
import { UsersService } from 'src/users/users.service'
WebSocketGateway,
} from "@nestjs/websockets";
import { Games } from "./game/Games";
import { GAME_EVENTS } from "./game/constants";
import { GameCreationDtoValidated } from "./dtos/GameCreationDtoValidated";
import { type Game } from "./game/Game";
import { plainToClass } from "class-transformer";
import { PointDtoValidated } from "./dtos/PointDtoValidated";
import { StringDtoValidated } from "./dtos/StringDtoValidated";
import { MatchmakingQueue } from "./game/MatchmakingQueue";
import { MatchmakingDtoValidated } from "./dtos/MatchmakingDtoValidated";
import { PongService } from "./pong.service";
import { UsersService } from "src/users/users.service";
@WebSocketGateway({
cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ }
cors: { origin: /^(http|ws):\/\/localhost(:\d+)?$/ },
})
export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor(
@ -30,12 +30,12 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly usersService: UsersService
) {}
private readonly games: Games = new Games(this.pongService)
private readonly socketToPlayerName = new Map<Socket, string>()
private readonly matchmakingQueue = new MatchmakingQueue(this.games)
private readonly games: Games = new Games(this.pongService);
private readonly socketToPlayerName = new Map<Socket, string>();
private readonly matchmakingQueue = new MatchmakingQueue(this.games);
playerIsRegistered(name: string): boolean {
return Array.from(this.socketToPlayerName.values()).includes(name)
return Array.from(this.socketToPlayerName.values()).includes(name);
}
handleConnection(): void {}
@ -44,15 +44,15 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket()
client: Socket
): void {
const name: string | undefined = this.socketToPlayerName.get(client)
const game: Game | undefined = this.games.playerGame(name)
const name: string | undefined = this.socketToPlayerName.get(client);
const game: Game | undefined = this.games.playerGame(name);
if (game !== undefined) {
game.stop()
game.stop();
}
if (name !== undefined) {
console.log('Disconnected ', this.socketToPlayerName.get(client))
this.matchmakingQueue.removePlayer(name)
this.socketToPlayerName.delete(client)
console.log("Disconnected ", this.socketToPlayerName.get(client));
this.matchmakingQueue.removePlayer(name);
this.socketToPlayerName.delete(client);
}
}
@ -61,30 +61,30 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
async registerPlayer(
@ConnectedSocket()
client: Socket,
@MessageBody('playerName') playerName: StringDtoValidated,
@MessageBody('socketKey') socketKey: StringDtoValidated
): Promise<{ event: string, data: boolean }> {
let succeeded: boolean = false
const user = await this.usersService.findUserByName(playerName.value)
@MessageBody("playerName") playerName: StringDtoValidated,
@MessageBody("socketKey") socketKey: StringDtoValidated
): Promise<{ event: string; data: boolean }> {
let succeeded: boolean = false;
const user = await this.usersService.findUserByName(playerName.value);
if (
user !== null &&
user.socketKey === socketKey.value &&
!this.playerIsRegistered(playerName.value)
) {
this.socketToPlayerName.set(client, playerName.value)
succeeded = true
console.log('Registered player', playerName.value)
this.socketToPlayerName.set(client, playerName.value);
succeeded = true;
console.log("Registered player", playerName.value);
} 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)
getPlayerCount(@ConnectedSocket() client: Socket): void {
const name: string | undefined = this.socketToPlayerName.get(client)
const name: string | undefined = this.socketToPlayerName.get(client);
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));
}
}
@ -98,9 +98,9 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
const realPosition: PointDtoValidated = plainToClass(
PointDtoValidated,
position
)
const name: string | undefined = this.socketToPlayerName.get(client)
this.games.movePlayer(name, realPosition)
);
const name: string | undefined = this.socketToPlayerName.get(client);
this.games.movePlayer(name, realPosition);
}
@UsePipes(new ValidationPipe({ whitelist: true }))
@ -109,11 +109,11 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket()
client: Socket,
@MessageBody() gameCreationDto: GameCreationDtoValidated
): { event: string, data: boolean } {
): { event: string; data: boolean } {
const realGameCreationDto: GameCreationDtoValidated = plainToClass(
GameCreationDtoValidated,
gameCreationDto
)
);
if (this.socketToPlayerName.size >= 2) {
const player1Socket: Socket | undefined = Array.from(
@ -122,14 +122,14 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
(key) =>
this.socketToPlayerName.get(key) ===
realGameCreationDto.playerNames[0]
)
);
const player2Socket: Socket | undefined = Array.from(
this.socketToPlayerName.keys()
).find(
(key) =>
this.socketToPlayerName.get(key) ===
realGameCreationDto.playerNames[1]
)
);
if (
player1Socket !== undefined &&
@ -137,34 +137,34 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
(client.id === player1Socket.id || client.id === player2Socket.id) &&
player1Socket.id !== player2Socket.id
) {
this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[0])
this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[1])
this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[0]);
this.matchmakingQueue.removePlayer(realGameCreationDto.playerNames[1]);
const ranked = false
const ranked = false;
this.games.newGame(
[player1Socket, player2Socket],
[player1Socket.id, player2Socket.id],
realGameCreationDto,
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)
ready(
@ConnectedSocket()
client: Socket
): { event: string, data: boolean } {
let succeeded: boolean = false
const name: string | undefined = this.socketToPlayerName.get(client)
): { event: string; data: boolean } {
let succeeded: boolean = false;
const name: string | undefined = this.socketToPlayerName.get(client);
if (name !== undefined) {
this.games.ready(name)
succeeded = true
this.games.ready(name);
succeeded = true;
}
return { event: GAME_EVENTS.READY, data: succeeded }
return { event: GAME_EVENTS.READY, data: succeeded };
}
@UsePipes(new ValidationPipe({ whitelist: true }))
@ -173,21 +173,21 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket()
client: Socket,
@MessageBody() matchmakingUpdateData: MatchmakingDtoValidated
): { event: string, data: MatchmakingDtoValidated } {
let matchmaking: boolean = false
const name: string | undefined = this.socketToPlayerName.get(client)
): { event: string; data: MatchmakingDtoValidated } {
let matchmaking: boolean = false;
const name: string | undefined = this.socketToPlayerName.get(client);
if (name !== undefined) {
if (matchmakingUpdateData.matchmaking && !this.games.isInAGame(name)) {
this.matchmakingQueue.addPlayer(name, client, client.id)
this.matchmakingQueue.addPlayer(name, client, client.id);
} else {
this.matchmakingQueue.removePlayer(name)
this.matchmakingQueue.removePlayer(name);
}
matchmaking = this.matchmakingQueue.isInQueue(name)
matchmaking = this.matchmakingQueue.isInQueue(name);
}
return {
event: GAME_EVENTS.MATCHMAKING,
data: { matchmaking }
}
data: { matchmaking },
};
}
@UsePipes(new ValidationPipe({ whitelist: true }))
@ -196,9 +196,9 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket()
client: Socket
): void {
const name: string | undefined = this.socketToPlayerName.get(client)
const name: string | undefined = this.socketToPlayerName.get(client);
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 { PongGateway } from './pong.gateway'
import Result from './entity/result.entity'
import { TypeOrmModule } from '@nestjs/typeorm'
import { PongService } from './pong.service'
import { UsersModule } from 'src/users/users.module'
import { PongController } from './pong.controller'
import { forwardRef, Module } from "@nestjs/common";
import { PongGateway } from "./pong.gateway";
import Result from "./entity/result.entity";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PongService } from "./pong.service";
import { UsersModule } from "src/users/users.module";
import { PongController } from "./pong.controller";
@Module({
imports: [forwardRef(() => UsersModule), TypeOrmModule.forFeature([Result])],
providers: [PongGateway, PongService],
controllers: [PongController],
exports: [PongService]
exports: [PongService],
})
export class PongModule {}

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

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

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

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

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

@ -1,8 +1,8 @@
declare module 'passport-42' {
export type Profile = any
export type VerifyCallback = any
declare module "passport-42" {
export type Profile = any;
export type VerifyCallback = any;
export class Strategy {
constructor (options: any, verify: any)
authenticate (req: any, options: any): any
constructor(options: any, verify: 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 { Express } from 'express'
import { ApiProperty } from "@nestjs/swagger";
import { Express } from "express";
export class UserDto {
@IsPositive()
@IsOptional()
readonly ftId: number
readonly ftId: number;
@IsString()
@IsNotEmpty()
readonly username: string
readonly username: string;
@IsOptional()
readonly status: string
readonly status: string;
@IsOptional()
readonly avatar: string
readonly avatar: string;
@IsOptional()
readonly authToken: string
readonly authToken: string;
@IsOptional()
readonly isVerified: boolean
readonly isVerified: boolean;
}
export class AvatarUploadDto {
@ApiProperty({ type: 'string', format: 'binary' })
file: Express.Multer.File
@ApiProperty({ type: "string", format: "binary" })
file: Express.Multer.File;
}

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

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

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

@ -11,31 +11,29 @@ import {
Res,
StreamableFile,
BadRequestException,
Redirect
} from '@nestjs/common'
Redirect,
} from "@nestjs/common";
import { FileInterceptor } from '@nestjs/platform-express'
import { diskStorage } from 'multer'
import { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import { type User } from './entity/user.entity'
import { UsersService } from './users.service'
import { UserDto, AvatarUploadDto } from './dto/user.dto'
import { PongService } from 'src/pong/pong.service'
import { type User } from "./entity/user.entity";
import { UsersService } from "./users.service";
import { UserDto, AvatarUploadDto } from "./dto/user.dto";
import { PongService } from "src/pong/pong.service";
import { AuthenticatedGuard } from 'src/auth/42-auth.guard'
import { Profile42 } from 'src/auth/42.decorator'
import { Profile } from 'passport-42'
import { AuthenticatedGuard } from "src/auth/42-auth.guard";
import { Profile42 } from "src/auth/42.decorator";
import { Profile } from "passport-42";
import { ApiBody, ApiConsumes } from '@nestjs/swagger'
import { type Request, Response } from 'express'
import { createReadStream } from 'fs'
import { join } from 'path'
import { ApiBody, ApiConsumes } from "@nestjs/swagger";
import { type Request, Response } from "express";
import { createReadStream } from "fs";
import { join } from "path";
@Controller("users")
export class UsersController {
constructor (
private readonly usersService: UsersService,
) {}
constructor(private readonly usersService: UsersService) {}
@UseGuards(AuthenticatedGuard)
@Post("block/:id")
@ -60,135 +58,141 @@ export class UsersController {
return await this.usersService.findUsers()
}
@Get('online')
@Get("online")
async getOnlineUsers(): Promise<User[]> {
return await this.usersService.findOnlineUsers()
return await this.usersService.findOnlineUsers();
}
@Get('friends')
@Get("friends")
@UseGuards(AuthenticatedGuard)
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)
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)
async getLeaderboard(): Promise<User[]> {
return await this.usersService.getLeaderboard()
return await this.usersService.getLeaderboard();
}
@Post('avatar')
@Post("avatar")
@UseGuards(AuthenticatedGuard)
@Redirect('http://localhost')
@Redirect("http://localhost")
@UseInterceptors(
FileInterceptor('avatar', {
FileInterceptor("avatar", {
storage: diskStorage({
destination: 'avatars/'
destination: "avatars/",
}),
fileFilter: (request: Request, file: Express.Multer.File, callback) => {
<<<<<<< HEAD
if (!file.mimetype.includes('image')) {
callback(null, false)
return
}
callback(null, true)
}
=======
if (!file.mimetype.includes("image")) {
callback(null, false);
return;
>>>>>>> ouai c un peu la merde mais bon
}
callback(null, true);
},
})
)
@ApiConsumes('multipart/form-data')
@ApiConsumes("multipart/form-data")
@ApiBody({
description: 'A new avatar for the user',
type: AvatarUploadDto
description: "A new avatar for the user",
type: AvatarUploadDto,
})
async changeAvatar(
@Profile42() profile: Profile,
@UploadedFile() file: Express.Multer.File
): Promise<void> {
if (file === undefined) return
await this.usersService.addAvatar(profile.id, file.filename)
if (file === undefined) return;
await this.usersService.addAvatar(profile.id, file.filename);
}
@Get('avatar')
@Get("avatar")
@UseGuards(AuthenticatedGuard)
async getAvatar(
@Profile42() profile: Profile,
@Res({ passthrough: true }) response: Response
): Promise<StreamableFile> {
return await this.getAvatarById(profile.id, response)
return await this.getAvatarById(profile.id, response);
}
@Get(':name/byname')
async getUserByName (@Param('name') username: string): Promise<User> {
const user = await this.usersService.findUserByName(username)
user.socketKey = ''
return user
@Get(":name/byname")
async getUserByName(@Param("name") username: string): Promise<User> {
const user = await this.usersService.findUserByName(username);
user.socketKey = "";
return user;
}
@Get('invit/:username')
@Get("invit/:username")
@UseGuards(AuthenticatedGuard)
async invitUser(
@Profile42() profile: Profile,
@Param('username') username: string
@Param("username") username: string
): Promise<void> {
const target: User | null = await this.usersService.findUserByName(
username
)
);
if (target === null) {
throw new BadRequestException(`User ${username} not found.`)
throw new BadRequestException(`User ${username} not found.`);
}
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)
if (ret !== 'OK') throw new BadRequestException(ret)
const ret: string = await this.usersService.invit(profile.id, target.ftId);
if (ret !== "OK") throw new BadRequestException(ret);
}
@Get(':id/avatar')
@Get(":id/avatar")
async getAvatarById(
@Param('id', ParseIntPipe) ftId: number,
@Param("id", ParseIntPipe) ftId: number,
@Res({ passthrough: true }) response: Response
): Promise<StreamableFile> {
const user: User | null = await this.usersService.findUser(ftId)
if (user === null) throw new BadRequestException('User unknown.')
const filename = user.avatar
const stream = createReadStream(join(process.cwd(), 'avatars/' + filename))
const user: User | null = await this.usersService.findUser(ftId);
if (user === null) throw new BadRequestException("User unknown.");
const filename = user.avatar;
const stream = createReadStream(join(process.cwd(), "avatars/" + filename));
response.set({
'Content-Diposition': `inline; filename='${filename}'`,
'Content-Type': 'image/jpg'
})
return new StreamableFile(stream)
"Content-Diposition": `inline; filename='${filename}'`,
"Content-Type": "image/jpg",
});
return new StreamableFile(stream);
}
@Get(':id')
@Get(":id")
async getUserById(
@Param('id', ParseIntPipe) ftId: number
@Param("id", ParseIntPipe) ftId: number
): Promise<User | null> {
const user = await this.usersService.findUser(ftId)
if (user == null) throw new BadRequestException('User unknown.')
user.socketKey = ''
return user
const user = await this.usersService.findUser(ftId);
if (user == null) throw new BadRequestException("User unknown.");
user.socketKey = "";
return user;
}
@Post(':id')
@Post(":id")
@UseGuards(AuthenticatedGuard)
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) {
await this.usersService.update(user, payload)
await this.usersService.update(user, payload);
} else {
await this.usersService.create(payload)
await this.usersService.create(payload);
}
}
@Get()
@UseGuards(AuthenticatedGuard)
async getUser(@Profile42() profile: Profile): Promise<User | null> {
return await this.usersService.findUser(profile.id)
return await this.usersService.findUser(profile.id);
}
@Post()
@ -197,8 +201,8 @@ export class UsersController {
@Body() payload: UserDto,
@Profile42() profile: Profile
): Promise<BadRequestException | User | null> {
const user = await this.usersService.findUser(profile.id)
if (user == null) throw new BadRequestException('User not found.')
return await this.usersService.update(user, payload)
const user = await this.usersService.findUser(profile.id);
if (user == null) throw new BadRequestException("User not found.");
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 { TypeOrmModule } from '@nestjs/typeorm'
import { User } from './entity/user.entity'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
import { PongModule } from 'src/pong/pong.module'
import { ChatModule } from 'src/chat/chat.module'
import { forwardRef, Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./entity/user.entity";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
import { PongModule } from "src/pong/pong.module";
import { ChatModule } from "src/chat/chat.module";
@Module({
imports: [
forwardRef(() => PongModule),
TypeOrmModule.forFeature([User])],
imports: [forwardRef(() => PongModule), TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService]
exports: [UsersService],
})
export class UsersModule {}

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

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

5
front/volume/src/App.svelte

@ -182,7 +182,10 @@
on:keydown={() =>
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>
{/if}

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

@ -8,14 +8,20 @@
}
import { onMount } from "svelte";
import { API_URL, store } from "../Auth";
import { io } from "../socket";
</script>
<script lang="ts">
//--------------------------------------------------------------------------------/
const joinChannel = async (id: number) => {
io.emit("joinChannel", id, $store.ftId);
};
let channels: Array<ChannelsType> = [];
onMount(async () => {
const res = await fetch(API_URL + "/channels", {
cors: "include",
credentials: "include",
mode: "cors",
});
@ -28,6 +34,7 @@
const selectChat = (id: number) => {
const channel = channels.find((c) => c.id === id);
if (channel) {
joinChannel(id);
onSelectChannel(channel);
}
};
@ -118,7 +125,9 @@
//--------------------------------------------------------------------------------/
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", {
credentials: "include",
method: "POST",
@ -154,7 +163,9 @@
on:keydown={() => removeChannel(_channels.id)}>delete</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}
{:else}
<p>No channels available</p>

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

@ -4,19 +4,46 @@
author: string;
text: string;
}
import { createEventDispatcher, onMount } from "svelte";
import { createEventDispatcher, onDestroy, onMount } from "svelte";
import { store, API_URL } from "../Auth";
import type { Player } from "./Profile.svelte";
interface User {
username: string;
}
import { io } from "../socket"
import type { ChannelsType } from "./Channels.svelte";
import type { User } from "./Profile.svelte";
</script>
< script lang = "ts" >
//--------------------------------------------------------------------------------/
export let chatMessages: Array<chatMessagesType> = [];
let blockedUsers: Array<User> = [];
let chatMembers: Array<User> = [];
let chatMessages: Array<chatMessagesType> = [];
export let channel: ChannelsType;
let newText = "";
onMount(async () => {
let res = await fetch(API_URL + "/users/" + $store.ftId + "/blocked", {
credentials: "include",
mode: "cors",
});
if (res.ok) blockedUsers = await res.json();
res = await fetch(API_URL + "/channels/" + channel.id + "/members", {
credentials: "include",
mode: "cors",
});
if (res.ok) chatMembers = await res.json();
io.on("messages", (msgs: Array<chatMessagesType>) => {
chatMessages = msgs;
});
io.on("newMessage", (msg: chatMessagesType) => {
chatMessages = [...chatMessages.slice(-5 + 1), msg];
});
onDestroy(() => {
io.emit("leaveChannel", channel.id, $store.ftId);
});
});
//--------------------------------------------------------------------------------/
@ -27,7 +54,8 @@
author: $store.username,
text: newText,
};
chatMessages = [...chatMessages.slice(-5 + 1), newMessage];
chatMessages = [...chatMessages.slice(-5 + 1)];
io.emit("addMessage", channel.id, $store.ftId, newText);
newText = "";
const messagesDiv = document.querySelector(".messages");
if (messagesDiv) {
@ -58,127 +86,145 @@
function toggleChatMembers() {
showChatMembers = !showChatMembers;
}
let chatMembers: Array<User> = [
{ username: "user1" },
{ username: "user2" },
{ username: "user3" },
{ username: "user4" },
{ username: "user5" },
{ username: "user6" },
{ username: "user7" },
{ username: "user8" },
{ username: "user9" },
];
// let chatMembers: Array<Player> = [];
// async function getChatMembers() {
// console.log("Getting chat members");
// const res = await fetch(API_URL + "/channels/members", {
// mode: "cors",
// });
// chatMembers = await res.json();
// }
//--------------------------------------------------------------------------------/
const blockUser = async (username: string) => {};
const blockUser = 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 + "/users/block/" + data1.ftId, {
credentials: "include",
method: "POST",
mode: "cors",
});
const data2 = await res2.json();
if (res2.ok) {
alert("User blocked");
} else {
alert("Failed to block user");
}
};
//--------------------------------------------------------------------------------/
const unblockUser = 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 + "/users/unblock/" + data1.ftId, {
credentials: "include",
method: "DELETE",
mode: "cors",
});
const data2 = await res2.json();
if (res2.ok) {
alert("User unblocked");
} else {
alert("Failed to unblock user");
}
};
//--------------------------------------------------------------------------------/
const banUser = async (username: string) => {
// const prompt = window.prompt("Enter ban duration in seconds");
// 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", {
// method: "POST",
// mode: "cors",
// });
// const data2 = await res2.json();
// if (res2.ok) {
// alert("User banned");
// } else {
// alert("Failed to ban user");
// }
const prompt = window.prompt("Enter ban duration in seconds");
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 + "/ban", {
credentials: "include",
method: "POST",
mode: "cors",
});
const data2 = await res2.json();
if (res2.ok) {
io.emit("kickUser", channel.id, $store.ftId, data1.ftId);
alert("User banned");
} else {
alert("Failed to ban user");
}
};
//--------------------------------------------------------------------------------/
const kickUser = async (username: string) => {
// set-up channel joining and kicking
// 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",
// mode: "cors",
// });
// const data2 = await res2.json();
// if (res2.ok) {
// alert("User kicked");
// } else {
// alert("Failed to kick user");
// }
const res = await fetch(API_URL + "/users/" + username + "/byname", {
credentials: "include",
mode: "cors",
});
const kickedUser = await res.json();
io.emit("kickUser", channel.id, $store.ftId, kickedUser.ftId);
};
//--------------------------------------------------------------------------------/
const muteUser = async (username: string) => {
// use minutes prompt to determine mute duration
// const prompt = window.prompt("Enter mute duration in seconds");
// 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",
// mode: "cors",
// });
// const data2 = await res2.json();
// if (res2.ok) {
// alert("User muted");
// } else {
// alert("Failed to mute user");
// }
const prompt = window.prompt("Enter mute duration in seconds");
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 + "/mute", {
credentials: "include",
method: "POST",
mode: "cors",
});
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", {
// mode: "cors",
// });
// const data1 = await res1.json();
// const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/admin", {
// method: "POST",
// mode: "cors",
// });
// const data2 = await res2.json();
// if (res2.ok) {
// alert("User admined");
// } else {
// alert("Failed to admin user");
// }
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", {
// mode: "cors",
// });
// const data1 = await res1.json();
// const res2 = await fetch(API_URL + "/channels/" + data1.ftId + "/admin", {
// method: "DELETE",
// mode: "cors",
// });
// const data2 = await res2.json();
// if (res2.ok) {
// alert("User admin removed");
// } else {
// alert("Failed to remove admin user");
// }
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");
}
};
//--------------------------------------------------------------------------------/
@ -189,6 +235,7 @@
<div class="messages" >
{ #each chatMessages as message }
< p class="message" >
{ #if !blockedUsers.filter((user) => user.username == message.author).length }
< span
class="message-name"
on: click = {() => openProfile(message.author)}
@ -197,8 +244,11 @@
>
{ message.author }
< /span>: {message.text}
{
/if}
< /p>
{/each}
{
/each}
< /div>
{ #if showProfileMenu }
<div
@ -208,12 +258,14 @@
>
<ul>
<li>
<button on:click={() => dispatch("send-message", selectedUser)}
<button on: click = {() => dispatch("send-message", selectedUser)
}
> Send Message < /button
>
</li>
< li >
<button on:click={() => dispatch("view-profile", selectedUser)}
<button on: click = {() => dispatch("view-profile", selectedUser)
}
> View Profile < /button
>
</li>
@ -228,13 +280,18 @@
>
</li>
<li>
<!-- block only if not blocked -->
{ #if!blockedUsers.filter((user) => user.username = selectedUser).length }
<button on: click = {() => blockUser(selectedUser)}> Block User < /button>
{:else }
<button on: click = {() => unblockUser(selectedUser)}> Unblock User < /button>
{
/if}
< /li>
< li > <button on: click = { closeProfileMenu } > Close < /button></li >
</ul>
< /div>
{/if}
{
/if}
< form on: submit | preventDefault={ sendMessage }>
<input type="text" placeholder = "Type a message..." bind: value = { newText } />
<button>
@ -257,8 +314,10 @@
< li >
<p>
{ member.username }
<button on:click={() => banUser(member.username)}>ban</button>
<button on:click={() => kickUser(member.username)}
< button on: click = {() => banUser(member.username)
}> ban < /button>
< button on: click = {() => kickUser(member.username)
}
> kick < /button
>
<button on: click = {() => muteUser(member.username)}
@ -275,11 +334,13 @@
-----------------------------------------------------------------------------------
</p>
< /li>
{/each}
{
/each}
< /ul>
< /div>
< /div>
{/if}
{
/if}
< /div>
< /div>

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