Add files via upload

This commit is contained in:
Richard Gingrich
2024-08-06 15:53:05 -06:00
committed by GitHub
commit 5ca105d4f2
21 changed files with 4428 additions and 0 deletions

72
api/README.md Normal file
View File

@@ -0,0 +1,72 @@
# SENG 513 - UI / API
The following System Requirements and Installation steps are required for running the project locally. <br>
If you are accessing the project remotely ([vivideradicator.ca](http://www.vivideradicator.ca)), you may skip to the Usage section of this document. <br>
## System Requirements
Ensure that the following are already installed on your operating system of choice. <br>
> Node JS <br>
> NPM <br>
> MongoDB
## Installation
User Interface
1. Clone or download the code found within the GitHub SENG513-UI repository, to a path/of/your/choosing <br>
1b. This can be done from within VS Code, via `git clone https://github.com/Rafael1321/SENG513-UI.git` <br>
2. Using the command prompt, `cd ~path/of/your/choosing/SENG513-UI` will navigate you to the proper directory <br>
3. Install all required project dependencies using `npm i` <br>
4. Run the application with `npm start` <br>
</br>
API <br>
Note that you must be running MongoDB before beginning this step. <br>
Instructions can be found at https://www.mongodb.com/docs/manual/installation/ <br>
1. Clone or download the code found within the GitHub SENG513-API repository, to a path/of/your/choosing <br>
1b. This can be done from within VS Code, via `git clone https://github.com/Rafael1321/SENG513-API.git` <br>
2. Using the command prompt, `cd ~path/of/your/choosing/SENG513-API` will navigate you to the proper directory <br>
3. Install all required project dependencies using `npm i` <br>
4. Run the application with `npm start` <br>
## Usage
The following steps can be used to experience the full functionality of the application. </br>
1. You will be greeted by the login/account registration page <br>
1b. If you do not yet have an account on DuoFinder, feel free to create one now. The 2 starter accounts below may also be used <br>
DO NOT use real password information here, it is not yet encrypted <br>
1c. Profile 1: email = vivideradicator@gmail.com password = seng513grading <br>
1d. Profile 2: email = me@vivideradicator.ca password = seng513grading <br>
2. Enter your email address and passwords into their respective fields <br>
3. Once logged in, the profile card can be seen. Clicking the edit button in the top right will enable customizing of your profile <br>
3b. You may change your profile picture, bio, gender, and age. Customize your profile as desired, and save the changes by pressing the same edit button again <br>
3c. Set your desired filters by pushing the 'Chat Filters' button, which will limit who you can match with based on your wishes <br>
4. To connect with another user, press the 'Find Duo' button <br>
4b. If you are wishing to test the functionality by yourself, simply log into another account on a seperate window and search for a match on both accounts <br>
5. Once connected, you may send messages in real-time to the other user. Pushing the 'Share Contact' button will automatically send your in-game information to the other user <br>
5b. Move onto another connection with the "Next" button, or return to your profile page by clicking the 'x' in the top-right corner <br>
6. Once back on your profile page, view your past matches with the 'Chat History' button <br>
6b. You may re-read past messages and give a rating to anyone you have talked to previously. To give a rating, press the top-right 'Rate Player' button <br>
7. Enjoy!
## Version History
*v1.0* <br>
Front-end only. Focus on creating a visually appealing and easy-to-use UI. <br>
Use-state-based system, where changes were not saved and lost upon refreshing/exiting the app. <br>
<br>
*v2.0* </br>
Introduced save states and a backend. User information now saved. <br>
*v2.1* </br>
Real-time data share between users made possible with sockets. <br>
<br>
*v3.0* <br>
Polishing of the app, squashing of several major bugs. <br>
Small UI adjustments, ensuring all systems work as they should. <br>
<br>
## Contributors and Credit
Gaganjot Brar <br>
Harkamal Randhawa <br>
Martha Ibarra <br>
Rafael Flores Souza <br>
Richard Gingrich <br>
Tyler Chen

45
api/controllers/chats.js Normal file
View File

@@ -0,0 +1,45 @@
const chats = require("../models/chat");
const _ = require('lodash');
module.exports.saveMessage = async (req, res) => {
try{
const {senderId, receiverId, message} = req.body;
if(!senderId) return res.type('json').status(400).send("Sender id was not provided.");
if(!receiverId) return res.type('json').status(400).send("Receiver id was not provided.");
if(!message) return res.type('json').status(400).send("Message was not provided");
const newChat = new chats({
senderId:senderId,
receiverId:receiverId,
message:message
});
await newChat.save();
return res.type('json').status(201).send(newChat);
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
};
module.exports.retrieveMessages = async (req, res) => {
try{
const {senderId, receiverId} = req.body;
if(!senderId) return res.type('json').status(400).send("Sender id was not provided.");
if(!receiverId) return res.type('json').status(400).send("Receiver id was not provided.");
const messages = [];
const cursor = chats.find({$or : [{$and:[{senderId:senderId}, {receiverId:receiverId}]}, {$and:[{senderId:receiverId}, {receiverId:senderId}]}]}).sort("timestamp").cursor();
for (let message = await cursor.next(); message != null; message = await cursor.next()) {
messages.push({senderId:message.senderId, receiverId:message.receiverId, message:message.message});
}
return res.type('json').status(200).send(messages);
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
}

View File

@@ -0,0 +1,45 @@
const commendation = require("../models/commendation");
const user = require("../models/user");
module.exports.saveCommendation = async (req, res) => {
try{
const {commenderId, commendedId, score} = req.body;
if(!commenderId) return res.type('json').status(400).send("Commender id was not provided.");
if(!commendedId) return res.type('json').status(400).send("Commended id was not provided.");
if(!score) return res.type('json').status(400).send("Score was not provided.");
if(score <= 0 || score > 10) return res.type('json').status(400).send("Score must be >= 1 and <= 10");
// Make sure the person is not commending again
let foundCommendation = await commendation.findOne({$and: [{commendedId:commendedId}, {commenderId:commenderId}]}).exec();
if(foundCommendation) return res.type('json').status(400).send("You have already commended this user.");
// Compute average reputation of commended person
let totalReputation = score, totalNumOfCommends = 1, newReputation = 0;
const cursor = commendation.find({commendedId:commendedId}).cursor();
for (let commendation = await cursor.next(); commendation != null; commendation = await cursor.next()) {
totalReputation += commendation.score;
totalNumOfCommends += 1;
}
newReputation = Math.round(totalReputation/totalNumOfCommends);
// Save New Commendation
await new commendation({
commendedId: commendedId,
commenderId: commenderId,
score: score
}).save();
// Update users reputation
const updatedUser = await user.findOneAndUpdate({_id: commendedId},
{reputation:newReputation},
{returnOriginal: false});
if(!updatedUser) return res.type('json').status(400).send("Commended user reputation could not be updated.");
return res.type('json').status(200).send();
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
};

View File

@@ -0,0 +1,61 @@
const filter = require("../models/filter");
const _ = require('lodash');
module.exports.retrieve = async (req, res) => {
try{
const userId = req.params.userId;
// Basic Validation
if(!userId) return res.type('json').status(400).send('The user id is missing');
// Check if the filters exist
var searchedFilters = await filter.findOne({userId:userId}).exec();
if (!searchedFilters){
return res.type('json').status(404).send("Filters for specified user were not found.");
}else{
return res.type('json').status(200).send(searchedFilters);
}
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
}
module.exports.upsert = async(req, res) => {
try{
const {userId, filters} = req.body;
// Basic Validation
if(!userId) return res.type('json').status(400).send('The user id is missing');
// Check if the filters exist
var searchedFilters = await filter.findOne({userId:userId}).exec();
if(searchedFilters){ // Update the filters
if(!filters) return res.type('json').status(400).send('The filters are is missing');
const updatedFilters = await filter.findOneAndUpdate({userId:userId},
{...filters},
{returnOriginal: false});
return res.type('json').status(200).send(updatedFilters);
}else{ // Insert new filters [with default values]
const newFilters = new filter({
userId: userId,
});
await newFilters.save();
return res.type('json').status(201).send(newFilters);
}
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
}

View File

@@ -0,0 +1,29 @@
const matching = require("../models/matching");
const user = require("../models/user");
const _ = require('lodash');
module.exports.retrieveMatchHistory = async (req, res) => {
try{
const userId = req.params.userId;
// Basic Validation
if(!userId) return res.type('json').status(400).send('The user id is missing');
// Check if the filters exist
const userList = [];
const cursor = matching.find({ $or:[ {'firstUser': userId}, {'secondUser': userId} ]}).sort('timestamp').cursor();
for (let match = await cursor.next(); match != null; match = await cursor.next()) {
if(match.firstUser !== userId){
userList.push(await user.findOne({_id:match.firstUser}).exec());
}else if(match.secondUser !== userId){
userList.push(await user.findOne({_id:match.secondUser}).exec());
}
}
return res.type('json').status(200).send(userList);
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
}

188
api/controllers/users.js Normal file
View File

@@ -0,0 +1,188 @@
const bcrypt = require('bcrypt');
const user = require("../models/user");
const _ = require('lodash');
const axios = require('axios');
module.exports.registerUser = async (req, res) => {
try{
const{displayName, gameName, tagLine, email, password, avatarImage} = req.body;
// Validation of body variables
if(!gameName) return res.type('json').status(404).send('The game name is missing');
if(!tagLine) return res.type('json').status(404).send('The tag line is missing');
if(!email) return res.type('json').status(404).send('The email is missing');
if(!password) return res.type('json').status(404).send('The password is missing');
let searchedUser = await user.findOne({gameName:gameName, tagLine:tagLine}).exec();
if(searchedUser) return res.type('json').status(400).send("Game name and tagline combination already in use.");
// Getting user's extra info from riot's api
const baseUrl1 = process.env.RIOT_BASE_URL1;
const fullUrl1 = `${baseUrl1}${gameName}\\${tagLine}`
let response = await axios.get(fullUrl1);
var extraData = response.data.data;
// Getting user's rank from riot's api
const baseUrl2 = process.env.RIOT_BASE_URL2;
const fullUrl2 = `${baseUrl2}${extraData.region}\\${gameName}\\${tagLine}`;
let response2 = await axios.get(fullUrl2);
let rank = parseRank(response2.data.data.currenttierpatched);
const newUser = new user({
riotId: extraData.puuid,
displayName: !displayName?gameName:displayName,
gameName: gameName,
tagLine: tagLine,
email: email,
password: await bcrypt.hash(password, await bcrypt.genSalt(10)),
avatarImage: avatarImage,
rank: [rank.rankType, rank.rankLevel],
accountLevel: extraData.account_level,
region: toRegionNo(extraData.region)
});
await newUser.save();
return res.type('json').status(201).send( _.pick(newUser, ['_id', 'riotId', 'displayName', 'gameName','tagLine', 'email', 'avatarImage', 'rank', 'accountLevel', 'region', 'age', 'gender', 'reputation', 'playerType', 'aboutMe']));
}catch(err){
return res.type('json').status(500).send(err.toString());
}
}
module.exports.loginUser = async (req, res) => {
try{
const {email, password} = req.body;
if(!email) return res.type('json').status(404).send('The username is missing');
if(!password) return res.type('json').status(404).send('The password is missing');
// Check if the username exists
var searchedUser = await user.findOne({email:email}).select("+password").exec();
if (!searchedUser){
return res.type('json').status(404).send("User was not found.");
}else{
// Check if the passwords match
const isPasswordValid = await bcrypt.compare(password, searchedUser.password);
if(!isPasswordValid){
return res.type('json').status(404).send("The password is incorrect.");
}
return res.type('json').status(200).send(_.pick(searchedUser, ['_id', 'riotId', 'displayName', 'gameName','tagLine','email', 'avatarImage', 'rank', 'accountLevel', 'region', 'age', 'gender', 'reputation', 'playerType', 'aboutMe']));
}
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
};
module.exports.updateUser = async (req, res) => {
try{
const updateDTO = req.body;
if(!updateDTO.userId) return res.type('json').status(400).send('A user id must be provided');
// Check if the username exists
var searchedUser = await user.findOne({_id:updateDTO.userId}).exec();
if (!searchedUser){
return res.type('json').status(404).send("User to update was not found.");
}else{
let updateParams = _.pick(updateDTO, ['displayName', 'age', 'gender', 'playerType', 'aboutMe', 'avatarImage']);
const updatedUser = await user.findOneAndUpdate({_id:updateDTO.userId},
{...updateParams},
{returnOriginal: false});
return res.type('json').status(200).send(updatedUser);
}
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
};
// Implement end-point to retrieve a user by ID (we only have login)
module.exports.findUser = async (req, res) => {
try{
const userId = req.params.userId;
if(!userId) return res.type('json').status(400).send('Invalid user id provided.');
// Find the user
var searchedUser = await user.findOne({_id:userId}).exec();
if (!searchedUser){
return res.type('json').status(404).send("User was not found.");
}else{
return res.type('json').status(200).send(_.pick(searchedUser, ['_id', 'riotId', 'displayName', 'gameName','tagLine','email', 'avatarImage', 'rank', 'accountLevel', 'region', 'age', 'gender', 'reputation', 'playerType', 'aboutMe']));
}
}catch(err) {
return res.type('json').status(500).send(err.toString());
}
};
/* Helper Functions */
function toRegionNo(region){
let res = -1;
switch(region){
case 'na':
res = 0;
break;
case 'eu':
res = 1;
break;
case 'ap':
res = 2;
break;
case 'kr':
res = 3;
break;
}
return res;
}
function parseRank(rank){
let rankName = rank.substring(0, rank.indexOf(" "));
let rankType = -1;
let rankLevel = rank.substring(rank.indexOf(" ") + 1) * 1;
switch(rankName.toLowerCase()){
case 'iron':
rankType = 1;
break;
case 'bronze':
rankType = 2;
break;
case 'silver':
rankType = 3;
break;
case 'gold':
rankType = 4;
break;
case 'platinum':
rankType = 5;
break;
case 'diamond':
rankType = 6;
break;
case 'ascendant':
rankType = 7;
break;
case 'immortal':
rankType = 8;
break;
case 'radiant':
rankType = 9;
break;
}
return {rankType: rankType, rankLevel: rankLevel};
}

28
api/models/chat.js Normal file
View File

@@ -0,0 +1,28 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ChatsSchema = new Schema({
senderId: {
type: String,
required: true,
unique: false,
},
receiverId:{
type: String,
required: true,
unique: false
},
message: {
type: String,
required: true,
unique: false,
},
timestamp : {
type: Date,
required: false,
unique: false,
default: Date.now
}
});
module.exports = mongoose.model("Chats", ChatsSchema);

View File

@@ -0,0 +1,22 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CommendationsSchema = new Schema({
commenderId: {
type: String,
required: true,
unique: false,
},
commendedId:{
type: String,
required: true,
unique: false
},
score: {
type: Number,
required: true,
unique: false,
}
});
module.exports = mongoose.model("Commendations", CommendationsSchema);

42
api/models/filter.js Normal file
View File

@@ -0,0 +1,42 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const FiltersSchema = new Schema({
userId: {
type: String,
required: true,
unique: true,
},
serverPreference: {
type: Number,
required: false,
unique: false,
default: 0 // na
},
gameMode: {
type: Number,
required: false,
unique: false,
default: 1 // casual
},
rankDisparity:{
type: Array,
required: false,
unique: false,
default: [1, 1, 5, 2]
},
ageRange:{
type: Array,
required: false,
unique: false,
default: [18, 25]
},
genders: {
type: Array,
required: false,
unique: false,
default: [true, true, true, true]
}
});
module.exports = mongoose.model("Filters", FiltersSchema);

23
api/models/matching.js Normal file
View File

@@ -0,0 +1,23 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const MatchingSchema = new Schema({
firstUser: {
type: String,
required: true,
unique: false
},
secondUser: {
type: String,
required: true,
unique: false
},
timestamp : {
type: Date,
required: false,
unique: false,
default: Date.now
}
});
module.exports = mongoose.model("Matching", MatchingSchema);

17
api/models/socketModel.js Normal file
View File

@@ -0,0 +1,17 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const SocketSchema = new Schema({
userId: {
type: String,
required: true,
unique: true,
},
socketId: {
type: String,
required: true,
unique: true
}
});
module.exports = mongoose.model("SocketModel", SocketSchema);

89
api/models/user.js Normal file
View File

@@ -0,0 +1,89 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
riotId: {
type: String,
required: false,
unique: false
},
displayName: {
type: String,
required: false,
unique: false,
default: ""
},
gameName: {
type: String,
required: true,
unique: false
},
tagLine:{
type: String,
required: true,
unique: false
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
unique: false,
select: false
},
avatarImage: {
type: String,
required: true,
unique: false
},
rank : {
type: Array,
required: true,
unique: false
},
accountLevel : {
type: Number,
required: true ,
unique: false
},
region : {
type: Number,
required: true,
unique: false
},
age : {
type: Number,
required: false,
unique: false,
default: 0
},
gender : {
type: Number,
required: false,
unique: false,
default: -1
},
reputation : {
type: Number,
required: false,
unique: false,
default: 5 // In the middle
},
playerType : {
type: Number,
required: false,
unique: false,
default: 1 // Casual
},
aboutMe : {
type: String,
required: false,
unique: false,
default: ""
}
});
module.exports = mongoose.model("User", UserSchema);

3390
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
api/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "seng513-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Rafael1321/SENG513-API.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/Rafael1321/SENG513-API/issues"
},
"homepage": "https://github.com/Rafael1321/SENG513-API#readme",
"dependencies": {
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
"cors": "^2.8.5",
"dotenv": "^16.0.2",
"express": "^4.18.1",
"lodash": "^4.17.21",
"mongoose": "^6.6.1",
"nodemon": "^2.0.20",
"socket.io": "^4.5.3"
}
}

12
api/routes/chats.js Normal file
View File

@@ -0,0 +1,12 @@
const express = require("express");
const router = express.Router({ mergeParams: true });
const chats = require("../controllers/chats");
router.route('/chats/retrieve')
.post(chats.retrieveMessages);
router.route('/chats/save')
.post(chats.saveMessage);
module.exports = router;

View File

@@ -0,0 +1,9 @@
const express = require("express");
const router = express.Router({ mergeParams: true });
const commendations = require("../controllers/commendations");
router.route('/commendations')
.post(commendations.saveCommendation);
module.exports = router;

12
api/routes/filters.js Normal file
View File

@@ -0,0 +1,12 @@
const express = require("express");
const router = express.Router({ mergeParams: true });
const filters = require("../controllers/filters");
router.route('/filters/:userId')
.get(filters.retrieve);
router.route('/filters')
.post(filters.upsert);
module.exports = router;

9
api/routes/matchings.js Normal file
View File

@@ -0,0 +1,9 @@
const express = require("express");
const router = express.Router({ mergeParams: true });
const matchings = require("../controllers/matchings");
router.route('/matchings/:userId')
.get(matchings.retrieveMatchHistory);
module.exports = router;

18
api/routes/users.js Normal file
View File

@@ -0,0 +1,18 @@
const express = require("express");
const router = express.Router({ mergeParams: true });
const users = require("../controllers/users");
router.route('/users/:userId')
.get(users.findUser);
router.route('/users/register')
.post(users.registerUser);
router.route('/users/login')
.post(users.loginUser);
router.route('/users/update')
.put(users.updateUser);
module.exports = router;

68
api/server.js Normal file
View File

@@ -0,0 +1,68 @@
const express = require("express");
const mongoose = require("mongoose");
const dotenv = require('dotenv');
const path = require('path');
const { Server } = require("socket.io");
const { createServer } = require('http');
const {broadcasting} = require('./sockets/broadcasting');
const cors=require("cors");
// DotEnv Configuration
dotenv.config();
// Mongo DB Configuration
const dbConfig = {
database : process.env.MONGO_DB ?? '',
hostname : process.env.MONGO_HOST ?? '',
port : process.env.MONGO_PORT ?? '',
};
const connectionURI = `mongodb://${dbConfig.hostname}:${dbConfig.port}/${dbConfig.database}`
mongoose.connect(connectionURI);
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", () => {
console.log("Database connected");
});
// App configuration
const appConfig = { port : process.env.APP_PORT ?? '5000' }
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors({
origin:'*',
credentials:true,
optionSuccessStatus:200,
}));
// Route Configuration
const userRoutes = require("./routes/users");
const filtersRoutes = require("./routes/filters");
const matchingRoutes = require("./routes/matchings");
const chatRoutes = require("./routes/chats");
const commendationRoutes = require("./routes/commendations");
app.use("/", userRoutes);
app.use("/", filtersRoutes);
app.use("/", matchingRoutes);
app.use("/", chatRoutes);
app.use("/", commendationRoutes)
// Starting the node.js server
app.listen(appConfig.port, () => {
console.log(`Serving express server on port ${appConfig.port}`);
});
// Socket configuration
const httpServer = createServer();
const io = new Server(httpServer);
broadcasting(io); // Used for sending and receiving simple real-time messages between users.
const socketConfig = { port : process.env.SOCKET_PORT ?? '2000'}
httpServer.listen(socketConfig.port, () => {
console.log(`Serving socket server on port ${socketConfig.port}`);
});

218
api/sockets/broadcasting.js Normal file
View File

@@ -0,0 +1,218 @@
const user = require("../models/user");
const matching = require("../models/matching");
const socketModel = require("../models/socketModel");
const filter = require("../models/filter");
// Used to keep track of who is connected and know their socket
const connectedUsers = new Map();
let matchingQueue = [];
function broadcasting(io){ // For connection and disconnection
io.on("connection", async (socket) => {
const connectedUserId = socket.handshake.query.userId;
if(!connectedUserId){
socket.emit(`error_user_connected`, {msg:`Invalid connected user id ${connectedUserId}.`});
return;
}
if(!connectedUsers.has(connectedUserId)){
connectedUsers.set(connectedUserId, socket.id);
// Check if entry exists in the db
const socketInfo = await socketModel.findOne({userId:connectedUserId}).exec();
if(socketInfo){
if(socketInfo.socketId !== socket.socketId){
await socketModel.updateOne({userId:connectedUserId}, {socketId:socket.id}).exec();
}
}else{
await new socketModel({
userId: connectedUserId,
socketId: socket.id
}).save();
}
// save in database
socket.emit(`success_user_connected`, {msg:`User with id ${connectedUserId} CONNECTED.`});
}else{
socket.emit(`error_user_connected`, {msg:`User with id ${connectedUserId} is already connected.`});
}
/* MATCHING */
socket.on('find_matching', async (findMatchDTO) => {
// In case user id is invalid.
if(!findMatchDTO.userId){
socket.emit('error_find_matching',{msg:'User id is invalid.'});
return;
}
if(!findMatchDTO.filters){
socket.emit('error_find_matching', {msg:"Filters are invalid."});
return;
}
// In case user id is not found
if(!connectedUsers.has(findMatchDTO.userId)){
socket.emit('error_find_matching', {msg:'User id does not exist.'});
return;
}
socket.emit('success_find_matching', {msg:`User with id ${findMatchDTO.userId} ONLINE.`});
// Attempting to find a match
let matchFound = -1;
const user1 = await user.findOne({_id:findMatchDTO.userId}).exec();
const user1Filters = findMatchDTO.filters;
for(let i = 0; i < matchingQueue.length && matchFound == -1; i++){
var user2Id = matchingQueue[i];
matching.countDocuments( {$or: [ { $and:[ {'firstUser': findMatchDTO.userId}, {'secondUser': user2Id} ]}, { $and:[ {'firstUser': user2Id}, {'secondUser': findMatchDTO.userId} ]}]}).exec().then( count => {
if(!count){
user.findOne({_id:user2Id}).exec().then( user2 => {
filter.findOne({userId:user2Id}).exec().then( user2Filters => {
// Region Matched
const regionMatched = (user1.region === user2Filters.serverPreference) && (user2.region === user1Filters.serverPreference);
// Game Mode Type
const playerTypeMatched = (user1.playerType === user2Filters.gameMode) && (user2.playerType === user1Filters.gameMode);
// Rank Macthed
const rankMatchedUser1 = (user1.rank[0] >= user2Filters.rankDisparity[0] && user1.rank[0] <= user2Filters.rankDisparity[2]);
const rankMatchedUser2 = (user2.rank[0] >= user1Filters.rankDisparity[0] && user2.rank[0] <= user1Filters.rankDisparity[2]);
const rankMatched = rankMatchedUser1 && rankMatchedUser2;
// Age Matched
const ageMatched = (user1.age >= user2Filters.ageRange[0] && user1.age <= user2Filters.ageRange[1]) &&
(user2.age >= user1Filters.ageRange[0] && user2.age <= user1Filters.ageRange[1]);
// Gender Matched
let genderMatched = false;
if(user1.gender !== -1 && user2.gender !== -1){
genderMatched = user2Filters.genders[user1.gender] && user1Filters.genders[user2.gender];
}
// They are match based on filters...
if(regionMatched && playerTypeMatched && rankMatched && ageMatched && genderMatched){
// Notify each user of the match and remove the matched user from queue
socket.emit('match_found', {user:user2});
if(!connectedUsers.has(user2Id)){
socketModel.findOne({userId:receiverId}).exec().then( socketInfo => {
if(socketInfo){
connectedUsers.set(socketInfo.userId, socketInfo.socketId);
io.to(socketInfo.socketId).emit('match_found', {user:user1});
}
});
}else{
io.to(connectedUsers?.get(user2Id)).emit('match_found', {user:user1}); // TODO: add DB call if not in there
}
// Store matching in the DB
const newMatch = new matching({
firstUser: findMatchDTO.userId,
secondUser: user2Id,
});
newMatch.save();
// Remove matched user from queue
matchingQueue = immutableRemove(matchFound, matchingQueue);
matchFound = i;
}
});
});
}
});
}
if(matchFound === -1){
matchingQueue.push(findMatchDTO.userId);
}
});
socket.on('stop_matching', (userId) => {
// In case user id is invalid.
if(!userId){
socket.emit('error_stop_matching',{msg:'User id is invalid.'});
return;
}
// In case user id is not found
if(!connectedUsers.has(userId)){
socket.emit('error_stop_matching', {msg:'User id does not exist.'});
return;
}
// Removing user from the matching queue
matchingQueue = immutableRemove(matchingQueue.indexOf(userId), matchingQueue);
socket.emit('success_stop_matching', {msg:`User with id ${userId} OFFLINE`});
});
/* CHAT */
socket.on("send_msg" , async (receiverId, msg) => { // msg = Text
if(!receiverId){
socket.emit('error_send_msg', {msg:"Invalid receiver id."});
return;
}
if(!connectedUsers.has(receiverId)){
// Check if it exists in DB
const socketInfo = await socketModel.findOne({userId:receiverId}).exec();
if(!socketInfo){
socket.emit('error_send_msg', {msg:"User with that id is not online."})
return;
}else{
connectedUsers.set(receiverId, socketInfo.socketId);
io.to(socketInfo.socketId).emit("receive_msg", {msg:msg});
}
}else{
io.to(connectedUsers?.get(receiverId)).emit("receive_msg", {msg:msg});
}
});
/* DISCONNECT*/
socket.on("disconnect", (socket) => {
const connectedUserId = getUserIdFromSocketId(socket.id);
if(!connectedUserId) return;
// Remove user from map
if(connectedUsers.has(connectedUserId)){
connectedUsers.delete(connectedUserId);
}
});
});
}
/* HELPER FUNCTIONS */
function getUserIdFromSocketId(socketId){
let userId = "";
for (let [uId, info] of connectedUsers) {
if(info.socketId === socketId){
userId = uId;
break;
}
}
return userId;
}
function immutableRemove(idx, array){
if(idx < 0 || idx >= array.length) return array;
newArray = [];
for(let i = 0; i < array.length; i++){
if(idx !== i) newArray.push(array[i]);
}
return newArray;
}
module.exports = { broadcasting };