forked from hippoz/brainlet
Add linter and documentation for future gateway version
This commit is contained in:
parent
9ed67992a1
commit
1c1b710cde
18 changed files with 1860 additions and 842 deletions
29
.eslintrc.js
Normal file
29
.eslintrc.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"commonjs": true,
|
||||||
|
"es2021": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"double"
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
200
DOCS.md
Normal file
200
DOCS.md
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
# Waffle API docs (currently not implemented)
|
||||||
|
|
||||||
|
# Gateway
|
||||||
|
|
||||||
|
Waffle uses a WebSocket gateway to transmit various updates from the server to the client and vice-versa (new message, user disconnecting, connect to voice, etc.)
|
||||||
|
|
||||||
|
# Gateway Payload format
|
||||||
|
|
||||||
|
All payloads sent through the gateway are *string payloads*. They all start with a number represeting the "instruction", followed by a "@" character (to delimit the instruction number from the rest of the payload), after which comes the data for the specific instruction.
|
||||||
|
|
||||||
|
Example packet:
|
||||||
|
```json
|
||||||
|
23@this is my payload data
|
||||||
|
```
|
||||||
|
Note for the example above: The "this is my payload data" text should not be user controlled. If user controlled input is required, it must be sanitized and/or passed as a JSON field.
|
||||||
|
|
||||||
|
Packets can also have JSON as a payload:
|
||||||
|
```json
|
||||||
|
32@{"name":"Alice"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
The terms "channel" and "category" are used interchangeably.
|
||||||
|
|
||||||
|
## 0:HELLO
|
||||||
|
*Part of handshake, Server to client*
|
||||||
|
|
||||||
|
Sent by the server to the client as soon as possible after they connect to the gateway.
|
||||||
|
|
||||||
|
This payload can contain a JSON object with various information about the server. It should be relied on only when absolutely necessary.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
0@{}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1:YOO
|
||||||
|
*Part of handshake, Client to server*
|
||||||
|
|
||||||
|
Sent by the client as soon as possible after receiving the HELLO instruction.
|
||||||
|
|
||||||
|
|
||||||
|
JSON data format:
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| token | The authentication token |
|
||||||
|
| client | The name of the application the client is connecting from (e.g. browser_react_app) |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
1@{"token":"my totally real token","client":"amazing client"}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the token is invalid, or the connection is otherwise rejected, the client should be disconnected as soon as possible, and no YOO\_ACK should be sent.
|
||||||
|
|
||||||
|
## 2:YOO\_ACK
|
||||||
|
*Part of handshake, Server to client*
|
||||||
|
|
||||||
|
Sent by the server as soon as possible after processing the YOO payload from the client.
|
||||||
|
|
||||||
|
This payload contains an empty JSON object, that may have data added to it in later versions of this software.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
2@{}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3:ACTION\_CREATE\_MESSAGE
|
||||||
|
*Auth required, Client to server*
|
||||||
|
|
||||||
|
Sent by the client when sending a message.
|
||||||
|
|
||||||
|
JSON data format:
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| content | The text content of the message |
|
||||||
|
| channel | An object that contains "_id", the id of the channel to send the message to |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
3@{"content":"i hate you","channel_id":"totally real channel id"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4:EVENT\_NEW\_MESSAGE
|
||||||
|
*Auth required, Server to client*
|
||||||
|
|
||||||
|
Sent by the server when a new message is created in a channel the client is subscribed to.
|
||||||
|
|
||||||
|
JSON data format:
|
||||||
|
[Message object](#message-object)
|
||||||
|
|
||||||
|
## 5:ACTION\_UPDATE\_STATUS
|
||||||
|
*Auth required, Client to server*
|
||||||
|
|
||||||
|
Sent by the client when updating status. Usually, this packet is sent as soon as possible after getting YOO\_ACK to indicate the client is online.
|
||||||
|
|
||||||
|
JSON data format:
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| status | Can be 0(STATUS\_OFFLINE) or 1(STATUS\_ONLINE) |
|
||||||
|
| status_text | Custom status text, max 50 characters, min 1 non-whitespace character, trimmed |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
5@{"status":1,"status_text":"hello"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6:EVENT\_CHANNEL\_MEMBERS
|
||||||
|
|
||||||
|
*Auth required, Server to client*
|
||||||
|
|
||||||
|
Sent by the server after YOO\_ACK, and otherwise when the entire status needs to be updated. If only one user's status changes, use EVENT\_CHANNEL\_MEMBER\_UPDATE.
|
||||||
|
|
||||||
|
JSON data format (this ones tough):
|
||||||
|
Contains a JSON object, where each key is a channel id and each property is an array of [user presence objects](#user-presence-object).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
6@{"totallyrealid":[{_id:"totallyrealuserid",username:"totallyrealusername",status:1,status_text:"hello"}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7:EVENT\_CHANNEL\_MEMBER\_UPDATE
|
||||||
|
|
||||||
|
*Auth required, Server to client*
|
||||||
|
|
||||||
|
Sent by the server when the status of a member changes. Of course, the client has to be in a channel with that member in order to receive this update.
|
||||||
|
|
||||||
|
JSON data format:
|
||||||
|
It's just a [user presence object](#user-presence-object).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
7@{_id:"totallyrealuserid",username:"totallyrealusername",status:1,status_text:"hello"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 21:ACTION\_VOICE\_BEGIN\_SESSION
|
||||||
|
|
||||||
|
*Auth required, Client to server*
|
||||||
|
|
||||||
|
Sent by the client whenever it wants to initiate a voice session in a specific channel.
|
||||||
|
|
||||||
|
JSON data format:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| channel | An object that contains "_id", the id of the channel to connect to |
|
||||||
|
|
||||||
|
(unfinished)
|
||||||
|
|
||||||
|
## 22:ACTION\_VOICE\_BEGIN\_SESSION
|
||||||
|
|
||||||
|
*Auth required, Client to server*
|
||||||
|
|
||||||
|
Sent by the server when the voice session request has been accepted. If it is rejected, the client MUST be disconnected from the gateway as soon as possible.
|
||||||
|
|
||||||
|
JSON data format:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| reportTo | The IP and port of the voice server to connect to |
|
||||||
|
|
||||||
|
(unfinished)
|
||||||
|
|
||||||
|
# Objects and data types
|
||||||
|
|
||||||
|
## Message object
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| content | The text content of the message (max 2000 characters, min 1 character, trimmed) |
|
||||||
|
| channel | A [message channel object](#message-channel-object) |
|
||||||
|
| author | A [message author object](#message-author-object) |
|
||||||
|
|
||||||
|
## Message channel object
|
||||||
|
|
||||||
|
Used mostly for messages.
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| _id | The id of the channel |
|
||||||
|
| name | The name of the channel |
|
||||||
|
|
||||||
|
## Message author object
|
||||||
|
|
||||||
|
Used mostly for messages.
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| _id | The id of the user |
|
||||||
|
| name | The name of the user |
|
||||||
|
|
||||||
|
## User presence object
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| - | - |
|
||||||
|
| _id | The id of the user |
|
||||||
|
| name | The name of the user |
|
||||||
|
| status | The status of the user (see ACTION_UPDATE_STATUS) |
|
||||||
|
| status_text | The text status of the user (see ACTION_UPDATE_STATUS) |
|
|
@ -1,64 +1,64 @@
|
||||||
const User = require('../../models/User');
|
const User = require("../../models/User");
|
||||||
const secret = require('../../secret');
|
const secret = require("../../secret");
|
||||||
const config = require('../../config');
|
const config = require("../../config");
|
||||||
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
const redirect = (res, status=401, url=undefined) => {
|
const redirect = (res, status=401, url=undefined) => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
res.status(status).json({
|
res.status(status).json({
|
||||||
error: true,
|
error: true,
|
||||||
message: 'ERROR_ACCESS_DENIED'
|
message: "ERROR_ACCESS_DENIED"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.redirect(url);
|
res.redirect(url);
|
||||||
}
|
};
|
||||||
|
|
||||||
function authenticateEndpoint(callback, url=undefined, minPermissionLevel=config.roleMap.RESTRICTED) {
|
function authenticateEndpoint(callback, url=undefined, minPermissionLevel=config.roleMap.RESTRICTED) {
|
||||||
return (req, res) => {
|
return (req, res) => {
|
||||||
const token = req.cookies.token;
|
const token = req.cookies.token;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
redirect(res, 403, url);
|
redirect(res, 403, url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt.verify(token, secret.jwtPrivateKey, {}, async (err, data) => {
|
jwt.verify(token, secret.jwtPrivateKey, {}, async (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
redirect(res, 401, url);
|
redirect(res, 401, url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
redirect(res, 401, url);
|
redirect(res, 401, url);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.username) {
|
if (!data.username) {
|
||||||
redirect(res, 401, url);
|
redirect(res, 401, url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findByUsername(data.username);
|
const user = await User.findByUsername(data.username);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(res, 401, url);
|
redirect(res, 401, url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let permissionLevel = config.roleMap[user.role];
|
let permissionLevel = config.roleMap[user.role];
|
||||||
if (!permissionLevel) {
|
if (!permissionLevel) {
|
||||||
permissionLevel = 0;
|
permissionLevel = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permissionLevel < minPermissionLevel) {
|
if (permissionLevel < minPermissionLevel) {
|
||||||
redirect(res, 401, url);
|
redirect(res, 401, url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(req, res, user);
|
callback(req, res, user);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { authenticateEndpoint };
|
module.exports = { authenticateEndpoint };
|
|
@ -1,141 +1,141 @@
|
||||||
const User = require('../../models/User');
|
const User = require("../../models/User");
|
||||||
const Category = require('../../models/Category');
|
const Category = require("../../models/Category");
|
||||||
const Post = require('../../models/Post');
|
const Post = require("../../models/Post");
|
||||||
const config = require('../../config');
|
const config = require("../../config");
|
||||||
|
|
||||||
const { authenticateEndpoint } = require('./authfunctions');
|
const { authenticateEndpoint } = require("./authfunctions");
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require("mongoose");
|
||||||
const { body, param, validationResult } = require('express-validator');
|
const { body, param, validationResult } = require("express-validator");
|
||||||
const express = require('express');
|
const express = require("express");
|
||||||
|
|
||||||
const app = express.Router();
|
const app = express.Router();
|
||||||
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
||||||
|
|
||||||
const rateLimit = require("express-rate-limit");
|
const rateLimit = require("express-rate-limit");
|
||||||
|
|
||||||
const createLimiter = rateLimit({
|
const createLimiter = rateLimit({
|
||||||
windowMs: 2 * 60 * 1000,
|
windowMs: 2 * 60 * 1000,
|
||||||
max: 10,
|
max: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/category/create', [
|
app.post("/category/create", [
|
||||||
createLimiter,
|
createLimiter,
|
||||||
body('title').not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape()
|
body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape()
|
||||||
], authenticateEndpoint(async (req, res, user) => {
|
], authenticateEndpoint(async (req, res, user) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = req.body.title;
|
const title = req.body.title;
|
||||||
const category = await Category.create({
|
const category = await Category.create({
|
||||||
title: title,
|
title: title,
|
||||||
creator: user._id,
|
creator: user._id,
|
||||||
posts: []
|
posts: []
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_CATEGORY_CREATED',
|
message: "SUCCESS_CATEGORY_CREATED",
|
||||||
category: category.getPublicObject()
|
category: category.getPublicObject()
|
||||||
});
|
});
|
||||||
}, undefined, config.roleMap.USER));
|
}, undefined, config.roleMap.USER));
|
||||||
|
|
||||||
app.post('/post/create', [
|
app.post("/post/create", [
|
||||||
createLimiter,
|
createLimiter,
|
||||||
body('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }),
|
body("category").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }),
|
||||||
body('title').not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
|
body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
|
||||||
body('body').not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
|
body("body").not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
|
||||||
], authenticateEndpoint(async (req, res, user) => {
|
], authenticateEndpoint(async (req, res, user) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = req.body.category;
|
const category = req.body.category;
|
||||||
const title = req.body.title;
|
const title = req.body.title;
|
||||||
const content = req.body.body;
|
const content = req.body.body;
|
||||||
|
|
||||||
const post = new Post();
|
const post = new Post();
|
||||||
post.title = title;
|
post.title = title;
|
||||||
post.body = content;
|
post.body = content;
|
||||||
post.creator = user._id;
|
post.creator = user._id;
|
||||||
post.category = category;
|
post.category = category;
|
||||||
|
|
||||||
const r = await Category.updateOne({
|
const r = await Category.updateOne({
|
||||||
_id: category
|
_id: category
|
||||||
}, {
|
}, {
|
||||||
$push: { posts: post }
|
$push: { posts: post }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (r.n < 1) {
|
if (r.n < 1) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
error: true,
|
error: true,
|
||||||
message: 'ERROR_CATEGORY_NOT_FOUND'
|
message: "ERROR_CATEGORY_NOT_FOUND"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_POST_CREATED',
|
message: "SUCCESS_POST_CREATED",
|
||||||
post: {
|
post: {
|
||||||
_id: post._id
|
_id: post._id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, undefined, config.roleMap.USER));
|
}, undefined, config.roleMap.USER));
|
||||||
|
|
||||||
app.get('/category/:category/info', [
|
app.get("/category/:category/info", [
|
||||||
param('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
param("category").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||||
], authenticateEndpoint(async (req, res, user) => {
|
], authenticateEndpoint(async (req, res) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryId = req.params.category;
|
const categoryId = req.params.category;
|
||||||
const category = await Category.findById(categoryId).populate('posts.creator', User.getPulicFields());
|
const category = await Category.findById(categoryId).populate("posts.creator", User.getPulicFields());
|
||||||
|
|
||||||
// TODO: Implement subscribing to a channel and stuff
|
// TODO: Implement subscribing to a channel and stuff
|
||||||
const users = await User.find().sort({ _id: -1 }).limit(50).select(User.getPulicFields())
|
const users = await User.find().sort({ _id: -1 }).limit(50).select(User.getPulicFields());
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
error: true,
|
error: true,
|
||||||
message: 'ERROR_CATEGORY_NOT_FOUND'
|
message: "ERROR_CATEGORY_NOT_FOUND"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_CATEGORY_DATA_FETCHED',
|
message: "SUCCESS_CATEGORY_DATA_FETCHED",
|
||||||
category: category.getPublicObject(),
|
category: category.getPublicObject(),
|
||||||
userInfo: {
|
userInfo: {
|
||||||
userListLimit: 50,
|
userListLimit: 50,
|
||||||
users: users
|
users: users
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, undefined, config.roleMap.USER));
|
}, undefined, config.roleMap.USER));
|
||||||
|
|
||||||
app.get('/category/list', authenticateEndpoint(async (req, res, user) => {
|
app.get("/category/list", authenticateEndpoint(async (req, res) => {
|
||||||
let count = parseInt(req.query.count);
|
let count = parseInt(req.query.count);
|
||||||
if (!Number.isInteger(count)) {
|
if (!Number.isInteger(count)) {
|
||||||
count = 10;
|
count = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is probably not efficient
|
// TODO: This is probably not efficient
|
||||||
const categories = await Category.find().sort({ _id: -1 }).limit(count).select('-posts -__v').populate('creator', User.getPulicFields());
|
const categories = await Category.find().sort({ _id: -1 }).limit(count).select("-posts -__v").populate("creator", User.getPulicFields());
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_CATEGORY_LIST_FETCHED',
|
message: "SUCCESS_CATEGORY_LIST_FETCHED",
|
||||||
categories
|
categories
|
||||||
});
|
});
|
||||||
}, undefined, config.roleMap.USER));
|
}, undefined, config.roleMap.USER));
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
|
@ -1,17 +1,17 @@
|
||||||
const User = require('../../../models/User');
|
const User = require("../../../models/User");
|
||||||
const secret = require('../../../secret');
|
const secret = require("../../../secret");
|
||||||
const config = require('../../../config');
|
const config = require("../../../config");
|
||||||
const Category = require('../../../models/Category');
|
const Category = require("../../../models/Category");
|
||||||
const RateLimiter = require('./ratelimiter');
|
const RateLimiter = require("./ratelimiter");
|
||||||
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require("jsonwebtoken");
|
||||||
const siolib = require('socket.io');
|
const siolib = require("socket.io");
|
||||||
const uuid = require('uuid');
|
const uuid = require("uuid");
|
||||||
|
|
||||||
class GatewayServer {
|
class GatewayServer {
|
||||||
constructor(httpServer) {
|
constructor(httpServer) {
|
||||||
this._io = siolib(httpServer);
|
this._io = siolib(httpServer);
|
||||||
this._gateway = this._io.of('/gateway');
|
this._gateway = this._io.of("/gateway");
|
||||||
this.rateLimiter = new RateLimiter({
|
this.rateLimiter = new RateLimiter({
|
||||||
points: 5,
|
points: 5,
|
||||||
time: 1000,
|
time: 1000,
|
||||||
|
@ -19,15 +19,15 @@ class GatewayServer {
|
||||||
});
|
});
|
||||||
this.eventSetup();
|
this.eventSetup();
|
||||||
|
|
||||||
this._commandPrefix = '/';
|
this._commandPrefix = "/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayServer.prototype._sendSystemMessage = function(socket, message, category) {
|
GatewayServer.prototype._sendSystemMessage = function(socket, message, category) {
|
||||||
const messageObject = {
|
const messageObject = {
|
||||||
author: {
|
author: {
|
||||||
username: '__SYSTEM',
|
username: "__SYSTEM",
|
||||||
_id: '5fc69864f15a7c5e504c9a1f'
|
_id: "5fc69864f15a7c5e504c9a1f"
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
title: category.title,
|
title: category.title,
|
||||||
|
@ -37,78 +37,78 @@ GatewayServer.prototype._sendSystemMessage = function(socket, message, category)
|
||||||
_id: uuid.v4()
|
_id: uuid.v4()
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.emit('message', messageObject);
|
socket.emit("message", messageObject);
|
||||||
};
|
};
|
||||||
|
|
||||||
GatewayServer.prototype.notifyClientsOfUpdate = function(reason) {
|
GatewayServer.prototype.notifyClientsOfUpdate = function(reason) {
|
||||||
this._gateway.emit('refreshClient', { reason: reason || 'REFRESH' });
|
this._gateway.emit("refreshClient", { reason: reason || "REFRESH" });
|
||||||
};
|
};
|
||||||
|
|
||||||
GatewayServer.prototype._processCommand = async function(socket, message) {
|
GatewayServer.prototype._processCommand = async function(socket, message) {
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
const fullCommandString = content.slice(this._commandPrefix.length, content.length);
|
const fullCommandString = content.slice(this._commandPrefix.length, content.length);
|
||||||
const fullCommand = fullCommandString.split(' ');
|
const fullCommand = fullCommandString.split(" ");
|
||||||
const command = fullCommand[0] || 'INVALID_COMMAND';
|
const command = fullCommand[0] || "INVALID_COMMAND";
|
||||||
const args = fullCommand.length - 1;
|
const args = fullCommand.length - 1;
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'INVALID_COMMAND': {
|
case "INVALID_COMMAND": {
|
||||||
this._sendSystemMessage(socket, 'Invalid command.', message.category);
|
this._sendSystemMessage(socket, "Invalid command.", message.category);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'admin/fr': {
|
case "admin/fr": {
|
||||||
if (args === 1) {
|
if (args === 1) {
|
||||||
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
|
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
|
||||||
this._gateway.emit('refreshClient', { reason: fullCommand[1] || 'REFRESH' });
|
this._gateway.emit("refreshClient", { reason: fullCommand[1] || "REFRESH" });
|
||||||
} else {
|
|
||||||
this._sendSystemMessage(socket, 'how about no', message.category);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this._sendSystemMessage(socket, 'Invalid number of arguments.', message.category);
|
this._sendSystemMessage(socket, "how about no", message.category);
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
|
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
|
||||||
}
|
}
|
||||||
case 'admin/fru': {
|
break;
|
||||||
if (args === 1) {
|
}
|
||||||
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
|
case "admin/fru": {
|
||||||
const user = await this._findSocketInRoom(message.category._id, fullCommand[1]);
|
if (args === 1) {
|
||||||
if (!user) {
|
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
|
||||||
this._sendSystemMessage(socket, 'User not found.', message.category);
|
const user = await this._findSocketInRoom(message.category._id, fullCommand[1]);
|
||||||
break;
|
if (!user) {
|
||||||
}
|
this._sendSystemMessage(socket, "User not found.", message.category);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
this._gateway.in(user.user.sid).emit('refreshClient', { reason: 'REFRESH' });
|
this._gateway.in(user.user.sid).emit("refreshClient", { reason: "REFRESH" });
|
||||||
} else {
|
|
||||||
this._sendSystemMessage(socket, 'how about no', message.category);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this._sendSystemMessage(socket, 'Invalid number of arguments.', message.category);
|
this._sendSystemMessage(socket, "how about no", message.category);
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
}
|
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
|
||||||
default: {
|
|
||||||
this._sendSystemMessage(socket, 'That command does not exist.', message.category);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
this._sendSystemMessage(socket, "That command does not exist.", message.category);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
GatewayServer.prototype.authDisconnect = function(socket, callback) {
|
GatewayServer.prototype.authDisconnect = function(socket, callback) {
|
||||||
console.log('[E] [gateway] [handshake] User disconnected due to failed authentication');
|
console.log("[E] [gateway] [handshake] User disconnected due to failed authentication");
|
||||||
socket.isConnected = false;
|
socket.isConnected = false;
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
socket.disconnect(true);
|
socket.disconnect(true);
|
||||||
callback(new Error('ERR_GATEWAY_AUTH_FAIL'));
|
callback(new Error("ERR_GATEWAY_AUTH_FAIL"));
|
||||||
};
|
};
|
||||||
|
|
||||||
GatewayServer.prototype.eventSetup = function() {
|
GatewayServer.prototype.eventSetup = function() {
|
||||||
this._gateway.use((socket, callback) => {
|
this._gateway.use((socket, callback) => {
|
||||||
console.log('[*] [gateway] [handshake] User authentication attempt');
|
console.log("[*] [gateway] [handshake] User authentication attempt");
|
||||||
socket.isConnected = false;
|
socket.isConnected = false;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (socket.isConnected) return;
|
if (socket.isConnected) return;
|
||||||
console.log('[E] [gateway] [handshake] User still not connected after timeout, removing...');
|
console.log("[E] [gateway] [handshake] User still not connected after timeout, removing...");
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
socket.disconnect(true);
|
socket.disconnect(true);
|
||||||
}, config.gatewayStillNotConnectedTimeoutMS);
|
}, config.gatewayStillNotConnectedTimeoutMS);
|
||||||
|
@ -117,7 +117,7 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
const token = socket.handshake.query.token;
|
const token = socket.handshake.query.token;
|
||||||
|
|
||||||
if (!token) return this.authDisconnect(socket, callback);
|
if (!token) return this.authDisconnect(socket, callback);
|
||||||
if (!(typeof token === 'string')) return this.authDisconnect(socket, callback);
|
if (!(typeof token === "string")) return this.authDisconnect(socket, callback);
|
||||||
|
|
||||||
const allSockets = this._gateway.sockets;
|
const allSockets = this._gateway.sockets;
|
||||||
for (let [_, e] of allSockets) {
|
for (let [_, e] of allSockets) {
|
||||||
|
@ -155,10 +155,10 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._gateway.on('connection', (socket) => {
|
this._gateway.on("connection", (socket) => {
|
||||||
console.log(`[*] [gateway] [handshake] User ${socket.user.username} connected, sending hello and waiting for yoo...`);
|
console.log(`[*] [gateway] [handshake] User ${socket.user.username} connected, sending hello and waiting for yoo...`);
|
||||||
|
|
||||||
socket.emit('hello', {
|
socket.emit("hello", {
|
||||||
gatewayStillNotConnectedTimeoutMS: config.gatewayStillNotConnectedTimeoutMS,
|
gatewayStillNotConnectedTimeoutMS: config.gatewayStillNotConnectedTimeoutMS,
|
||||||
resolvedUser: {
|
resolvedUser: {
|
||||||
username: socket.user.username,
|
username: socket.user.username,
|
||||||
|
@ -166,14 +166,14 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.once('yoo', () => {
|
socket.once("yoo", () => {
|
||||||
console.log(`[*] [gateway] [handshake] Got yoo from ${socket.user.username}, connection is finally completed!`);
|
console.log(`[*] [gateway] [handshake] Got yoo from ${socket.user.username}, connection is finally completed!`);
|
||||||
socket.isConnected = true;
|
socket.isConnected = true;
|
||||||
|
|
||||||
socket.on('message', async ({ category, content, nickAuthor, destUser }) => {
|
socket.on("message", async ({ category, content, nickAuthor, destUser }) => {
|
||||||
if (!category || !content || !socket.joinedCategories || !socket.isConnected || !socket.user || !(typeof content === 'string') || !(typeof category._id === 'string')) return;
|
if (!category || !content || !socket.joinedCategories || !socket.isConnected || !socket.user || !(typeof content === "string") || !(typeof category._id === "string")) return;
|
||||||
content = content.trim();
|
content = content.trim();
|
||||||
if (!content || content === '' || content === ' ' || content.length >= 2000) return;
|
if (!content || content === "" || content === " " || content.length >= 2000) return;
|
||||||
if (!this.rateLimiter.consoom(socket.user.token)) { // TODO: maybe user ip instead of token?
|
if (!this.rateLimiter.consoom(socket.user.token)) { // TODO: maybe user ip instead of token?
|
||||||
console.log(`[E] [gateway] Rate limiting ${socket.user.username}`);
|
console.log(`[E] [gateway] Rate limiting ${socket.user.username}`);
|
||||||
return;
|
return;
|
||||||
|
@ -181,7 +181,7 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
|
|
||||||
// TODO: When/if category permissions are added, check if the user has permissions for that category
|
// TODO: When/if category permissions are added, check if the user has permissions for that category
|
||||||
const categoryTitle = socket.joinedCategories[category._id];
|
const categoryTitle = socket.joinedCategories[category._id];
|
||||||
if (!categoryTitle || !(typeof categoryTitle === 'string')) return;
|
if (!categoryTitle || !(typeof categoryTitle === "string")) return;
|
||||||
|
|
||||||
let messageObject = {
|
let messageObject = {
|
||||||
author: {
|
author: {
|
||||||
|
@ -197,14 +197,14 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
_id: uuid.v4()
|
_id: uuid.v4()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (nickAuthor && nickAuthor.username && (typeof nickAuthor.username) === 'string' && nickAuthor.username.length <= 32 && nickAuthor.username.length >= 3) {
|
if (nickAuthor && nickAuthor.username && (typeof nickAuthor.username) === "string" && nickAuthor.username.length <= 32 && nickAuthor.username.length >= 3) {
|
||||||
if (socket.user.permissionLevel === config.roleMap.BOT) {
|
if (socket.user.permissionLevel === config.roleMap.BOT) {
|
||||||
messageObject = {
|
messageObject = {
|
||||||
nickAuthor: {
|
nickAuthor: {
|
||||||
username: nickAuthor.username
|
username: nickAuthor.username
|
||||||
},
|
},
|
||||||
...messageObject
|
...messageObject
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,22 +213,22 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destUser && destUser._id && (typeof destUser._id) === 'string') {
|
if (destUser && destUser._id && (typeof destUser._id) === "string") {
|
||||||
const user = await this._findSocketInRoom(messageObject.category._id, destUser._id);
|
const user = await this._findSocketInRoom(messageObject.category._id, destUser._id);
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
this._gateway.in(user.user.sid).emit('message', messageObject);
|
this._gateway.in(user.user.sid).emit("message", messageObject);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._gateway.in(category._id).emit('message', messageObject);
|
this._gateway.in(category._id).emit("message", messageObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('subscribe', async (categories) => {
|
socket.on("subscribe", async (categories) => {
|
||||||
if ( !socket.isConnected || !socket.user || !categories || !Array.isArray(categories) || categories === []) return;
|
if ( !socket.isConnected || !socket.user || !categories || !Array.isArray(categories) || categories === []) return;
|
||||||
try {
|
try {
|
||||||
for (const v of categories) {
|
for (const v of categories) {
|
||||||
if (!v && !(typeof v === 'string')) continue;
|
if (!v && !(typeof v === "string")) continue;
|
||||||
// TODO: When/if category permissions are added, check if the user has permissions for that category
|
// TODO: When/if category permissions are added, check if the user has permissions for that category
|
||||||
const category = await Category.findById(v);
|
const category = await Category.findById(v);
|
||||||
if (category && category.title && category._id) {
|
if (category && category.title && category._id) {
|
||||||
|
@ -240,7 +240,7 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
console.log(`[*] [gateway] User ${socket.user.username} subscribed to room ${v} (${category.title}), sending updated user list to all members of that room...`);
|
console.log(`[*] [gateway] User ${socket.user.username} subscribed to room ${v} (${category.title}), sending updated user list to all members of that room...`);
|
||||||
|
|
||||||
const upd = await this._generateClientListUpdateObject(v, category.title);
|
const upd = await this._generateClientListUpdateObject(v, category.title);
|
||||||
this._gateway.in(v).emit('clientListUpdate', upd);
|
this._gateway.in(v).emit("clientListUpdate", upd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -248,18 +248,18 @@ GatewayServer.prototype.eventSetup = function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnecting', async () => {
|
socket.on("disconnecting", async () => {
|
||||||
console.log(`[*] [gateway] User ${socket.user.username} is disconnecting, broadcasting updated user list to all of the rooms they have been in...`);
|
console.log(`[*] [gateway] User ${socket.user.username} is disconnecting, broadcasting updated user list to all of the rooms they have been in...`);
|
||||||
const rooms = socket.rooms;
|
const rooms = socket.rooms;
|
||||||
rooms.forEach(async (room) => {
|
rooms.forEach(async (room) => {
|
||||||
// Socket io automatically adds a user to a room with their own id
|
// Socket io automatically adds a user to a room with their own id
|
||||||
if (room === socket.id) return;
|
if (room === socket.id) return;
|
||||||
|
|
||||||
const categoryTitle = socket.joinedCategories[room] || 'UNKNOWN';
|
const categoryTitle = socket.joinedCategories[room] || "UNKNOWN";
|
||||||
await socket.leave(room);
|
await socket.leave(room);
|
||||||
|
|
||||||
const upd = await this._generateClientListUpdateObject(room, categoryTitle);
|
const upd = await this._generateClientListUpdateObject(room, categoryTitle);
|
||||||
socket.in(room).emit('clientListUpdate', upd);
|
socket.in(room).emit("clientListUpdate", upd);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -309,7 +309,7 @@ GatewayServer.prototype._findSocketInRoom = async function(room, userid) {
|
||||||
return updatedClientList[0] || undefined;
|
return updatedClientList[0] || undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle='UNKNOWN') {
|
GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle="UNKNOWN") {
|
||||||
const clientList = await this._getSocketsInRoom(room);
|
const clientList = await this._getSocketsInRoom(room);
|
||||||
return {
|
return {
|
||||||
category: {
|
category: {
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
const usersAPI = require('./users');
|
const usersAPI = require("./users");
|
||||||
const contentAPI = require('./content');
|
const contentAPI = require("./content");
|
||||||
|
|
||||||
const express = require('express');
|
const express = require("express");
|
||||||
|
|
||||||
const app = express.Router();
|
const app = express.Router();
|
||||||
|
|
||||||
app.use('/users', usersAPI);
|
app.use("/users", usersAPI);
|
||||||
app.use('/content', contentAPI);
|
app.use("/content", contentAPI);
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
// TODO: Add more checks for this, or maybe remove
|
// TODO: Add more checks for this, or maybe remove
|
||||||
res.json({ apiStatus: 'OK', apis: [ 'users', 'content' ] });
|
res.json({ apiStatus: "OK", apis: [ "users", "content" ] });
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
430
api/v1/users.js
430
api/v1/users.js
|
@ -1,216 +1,216 @@
|
||||||
const User = require('../../models/User');
|
const User = require("../../models/User");
|
||||||
const config = require('../../config');
|
const config = require("../../config");
|
||||||
const secret = require('../../secret');
|
const secret = require("../../secret");
|
||||||
|
|
||||||
const { authenticateEndpoint } = require('./authfunctions');
|
const { authenticateEndpoint } = require("./authfunctions");
|
||||||
|
|
||||||
// TODO: Might want to use something else (https://blog.benpri.me/blog/2019/01/13/why-you-shouldnt-be-using-bcrypt-and-scrypt/)
|
// TODO: Might want to use something else (https://blog.benpri.me/blog/2019/01/13/why-you-shouldnt-be-using-bcrypt-and-scrypt/)
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require("bcrypt");
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require("mongoose");
|
||||||
const { body, query, param, validationResult } = require('express-validator');
|
const { body, param, validationResult } = require("express-validator");
|
||||||
const express = require('express');
|
const express = require("express");
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
const app = express.Router();
|
const app = express.Router();
|
||||||
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
||||||
|
|
||||||
const rateLimit = require("express-rate-limit");
|
const rateLimit = require("express-rate-limit");
|
||||||
|
|
||||||
const createAccountLimiter = rateLimit({
|
const createAccountLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000, // 1 hour window
|
windowMs: 60 * 60 * 1000, // 1 hour window
|
||||||
max: 10, // start blocking after 5 requests
|
max: 10, // start blocking after 5 requests
|
||||||
message: "You are being rate limited"
|
message: "You are being rate limited"
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/account/create/info', async (req, res) => {
|
app.get("/account/create/info", async (req, res) => {
|
||||||
let requiresCode = false;
|
let requiresCode = false;
|
||||||
if (config.restrictions) {
|
if (config.restrictions) {
|
||||||
const restrictions = config.restrictions.signup;
|
const restrictions = config.restrictions.signup;
|
||||||
if (restrictions && restrictions.specialCode) {
|
if (restrictions && restrictions.specialCode) {
|
||||||
requiresCode = true;
|
requiresCode = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_ACCOUNT_CREATE_INFO_FETCH',
|
message: "SUCCESS_ACCOUNT_CREATE_INFO_FETCH",
|
||||||
requiresSpecialCode: requiresCode
|
requiresSpecialCode: requiresCode
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/account/create', [
|
app.post("/account/create", [
|
||||||
createAccountLimiter,
|
createAccountLimiter,
|
||||||
body('username').not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(),
|
body("username").not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(),
|
||||||
body('email').not().isEmpty().isEmail().normalizeEmail(),
|
body("email").not().isEmpty().isEmail().normalizeEmail(),
|
||||||
body('password').not().isEmpty().isLength({ min: 8, max: 128 }),
|
body("password").not().isEmpty().isLength({ min: 8, max: 128 }),
|
||||||
body('specialCode').optional().isLength({ min: 12, max: 12 }).isAlphanumeric()
|
body("specialCode").optional().isLength({ min: 12, max: 12 }).isAlphanumeric()
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.restrictions) {
|
if (config.restrictions) {
|
||||||
const restrictions = config.restrictions.signup;
|
const restrictions = config.restrictions.signup;
|
||||||
if (restrictions && restrictions.specialCode) {
|
if (restrictions && restrictions.specialCode) {
|
||||||
const passedSpecialCode = req.body.specialCode;
|
const passedSpecialCode = req.body.specialCode;
|
||||||
const specialCode = restrictions.specialCode;
|
const specialCode = restrictions.specialCode;
|
||||||
|
|
||||||
if (passedSpecialCode && specialCode) {
|
if (passedSpecialCode && specialCode) {
|
||||||
if (specialCode !== passedSpecialCode) {
|
if (specialCode !== passedSpecialCode) {
|
||||||
res.status(401).json({ error: true, message: 'ERROR_REQUEST_SPECIAL_CODE_MISSING', errors: [{ msg: 'No specialCode passed', param: 'specialCode', location: 'body' }] });
|
res.status(401).json({ error: true, message: "ERROR_REQUEST_SPECIAL_CODE_MISSING", errors: [{ msg: "No specialCode passed", param: "specialCode", location: "body" }] });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ error: true, message: 'ERROR_REQUEST_SPECIAL_CODE_MISSING', errors: [{ msg: 'No specialCode passed', param: 'specialCode', location: 'body' }] });
|
res.status(401).json({ error: true, message: "ERROR_REQUEST_SPECIAL_CODE_MISSING", errors: [{ msg: "No specialCode passed", param: "specialCode", location: "body" }] });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = req.body.username;
|
const username = req.body.username;
|
||||||
|
|
||||||
const existingUser = await User.findByUsername(username);
|
const existingUser = await User.findByUsername(username);
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_USERNAME_EXISTS', errors: [{ value: username, msg: 'Username exists', param: 'username', location: 'body' }] });
|
res.status(400).json({ error: true, message: "ERROR_REQUEST_USERNAME_EXISTS", errors: [{ value: username, msg: "Username exists", param: "username", location: "body" }] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unhashedPassword = req.body.password;
|
const unhashedPassword = req.body.password;
|
||||||
const email = req.body.email;
|
const email = req.body.email;
|
||||||
const startingRole = 'USER';
|
const startingRole = "USER";
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(unhashedPassword, config.bcryptRounds);
|
const hashedPassword = await bcrypt.hash(unhashedPassword, config.bcryptRounds);
|
||||||
|
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
role: startingRole,
|
role: startingRole,
|
||||||
color: User.generateColorFromUsername(username)
|
color: User.generateColorFromUsername(username)
|
||||||
});
|
});
|
||||||
|
|
||||||
const userObject = await user.getPublicObject();
|
const userObject = await user.getPublicObject();
|
||||||
|
|
||||||
console.log('[*] [logger] [users] [create] User created', userObject);
|
console.log("[*] [logger] [users] [create] User created", userObject);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_USER_CREATED',
|
message: "SUCCESS_USER_CREATED",
|
||||||
user: userObject
|
user: userObject
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Internal server error', e);
|
console.error("Internal server error", e);
|
||||||
res.status(500).json({ error: true, message: 'INTERNAL_SERVER_ERROR' });
|
res.status(500).json({ error: true, message: "INTERNAL_SERVER_ERROR" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/token/create', [
|
app.post("/token/create", [
|
||||||
createAccountLimiter,
|
createAccountLimiter,
|
||||||
body('username').not().isEmpty().trim().isAlphanumeric(),
|
body("username").not().isEmpty().trim().isAlphanumeric(),
|
||||||
body('password').not().isEmpty()
|
body("password").not().isEmpty()
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
|
res.status(400).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = req.body.username;
|
const username = req.body.username;
|
||||||
|
|
||||||
const existingUser = await User.findByUsername(username);
|
const existingUser = await User.findByUsername(username);
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
res.status(403).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
|
res.status(403).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const password = req.body.password;
|
const password = req.body.password;
|
||||||
|
|
||||||
let passwordCheck;
|
let passwordCheck;
|
||||||
try {
|
try {
|
||||||
passwordCheck = await bcrypt.compare(password, existingUser.password);
|
passwordCheck = await bcrypt.compare(password, existingUser.password);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
passwordCheck = false;
|
passwordCheck = false;
|
||||||
}
|
}
|
||||||
if (!passwordCheck) {
|
if (!passwordCheck) {
|
||||||
res.status(403).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
|
res.status(403).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: '3h' }, async (err, token) => {
|
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: "3h" }, async (err, token) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: true,
|
error: true,
|
||||||
message: 'INTERNAL_SERVER_ERROR'
|
message: "INTERNAL_SERVER_ERROR"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Ugly fix for setting httponly cookies
|
// TODO: Ugly fix for setting httponly cookies
|
||||||
if (req.body.alsoSetCookie) {
|
if (req.body.alsoSetCookie) {
|
||||||
res.cookie('token', token, {
|
res.cookie("token", token, {
|
||||||
maxAge: 3 * 60 * 60 * 1000, httpOnly: true, domain: config.address,
|
maxAge: 3 * 60 * 60 * 1000, httpOnly: true, domain: config.address,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userObject = await existingUser.getPublicObject();
|
const userObject = await existingUser.getPublicObject();
|
||||||
|
|
||||||
console.log('[*] [logger] [users] [token create] Token created', userObject);
|
console.log("[*] [logger] [users] [token create] Token created", userObject);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_TOKEN_CREATED',
|
message: "SUCCESS_TOKEN_CREATED",
|
||||||
user: userObject,
|
user: userObject,
|
||||||
token
|
token
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/current/info', authenticateEndpoint(async (req, res, user) => {
|
app.get("/current/info", authenticateEndpoint(async (req, res, user) => {
|
||||||
const userObject = await user.getPublicObject();
|
const userObject = await user.getPublicObject();
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_USER_DATA_FETCHED',
|
message: "SUCCESS_USER_DATA_FETCHED",
|
||||||
user: {
|
user: {
|
||||||
token: req.cookies.token, // TODO: Passing the token like this is *terribly* insecure
|
token: req.cookies.token, // TODO: Passing the token like this is *terribly* insecure
|
||||||
...userObject
|
...userObject
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, undefined, 0));
|
}, undefined, 0));
|
||||||
|
|
||||||
app.get('/user/:userid/info', [
|
app.get("/user/:userid/info", [
|
||||||
param('userid').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
param("userid").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||||
], authenticateEndpoint(async (req, res, user) => {
|
], authenticateEndpoint(async (req, res) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userid = req.params.userid;
|
const userid = req.params.userid;
|
||||||
if (!userid) {
|
if (!userid) {
|
||||||
res.sendStatus(400).json({
|
res.sendStatus(400).json({
|
||||||
error: true,
|
error: true,
|
||||||
message: 'ERROR_REQUEST_INVALID_DATA'
|
message: "ERROR_REQUEST_INVALID_DATA"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const otherUser = await User.findById(userid);
|
const otherUser = await User.findById(userid);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
error: false,
|
error: false,
|
||||||
message: 'SUCCESS_USER_DATA_FETCHED',
|
message: "SUCCESS_USER_DATA_FETCHED",
|
||||||
user: await otherUser.getPublicObject(),
|
user: await otherUser.getPublicObject(),
|
||||||
});
|
});
|
||||||
}, undefined, config.roleMap.USER));
|
}, undefined, config.roleMap.USER));
|
||||||
|
|
||||||
app.post('/browser/token/clear', authenticateEndpoint((req, res, user) => {
|
app.post("/browser/token/clear", authenticateEndpoint((req, res) => {
|
||||||
res.clearCookie('token');
|
res.clearCookie("token");
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
15
api/v2/gateway/index.js
Normal file
15
api/v2/gateway/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const websockets = require("ws");
|
||||||
|
|
||||||
|
class GatewayServer {
|
||||||
|
constructor({ server }) {
|
||||||
|
this.wss = new websockets.Server({ server });
|
||||||
|
|
||||||
|
this.wss.on("connection", (ws) => {
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GatewayServer;
|
18
api/v2/gateway/messageparser.js
Normal file
18
api/v2/gateway/messageparser.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const config = require("../../../config");
|
||||||
|
|
||||||
|
const opcodes = {
|
||||||
|
0: "HELLO"
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseMessage = (message) => {
|
||||||
|
if (typeof message !== "string") throw new Error("GatewayMessageParser: Message is not a string");
|
||||||
|
if (message.length < 1) throw new Error("GatewayMessageParser: Message has less than 1 character");
|
||||||
|
if (message.length > config.gatewayMaxStringPayloadLength) throw new Error(`GatewayMessageParser: Message has more than ${config.gatewayMaxStringPayloadLength} characters`);
|
||||||
|
const op = parseInt(message[0]);
|
||||||
|
if (!op || isNaN(op)) throw new Error("GatewayMessageParser: Message has invalid opcode");
|
||||||
|
const opcodeName = opcodes[op];
|
||||||
|
if (!opcodeName) throw new Error("GatewayMessageParser: Message has unknown opcode");
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { parseMessage };
|
47
config.js
47
config.js
|
@ -1,22 +1,25 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ports: {
|
ports: {
|
||||||
mainServerPort: 3005,
|
mainServerPort: 3005,
|
||||||
},
|
},
|
||||||
address: 'localhost',
|
address: "localhost",
|
||||||
//restrictions: {
|
//restrictions: {
|
||||||
// signup: {
|
// signup: {
|
||||||
// specialCode: ''
|
// specialCode: ''
|
||||||
// }
|
// }
|
||||||
//},
|
//},
|
||||||
corsAllowList: [ 'localhost' ],
|
corsAllowList: [ "http://localhost:3000", "http://localhost:3005" ],
|
||||||
mongoUrl: 'mongodb://localhost:27017/app',
|
mongoUrl: "mongodb://192.168.0.105:27017/app",
|
||||||
bcryptRounds: 10,
|
|
||||||
roleMap: {
|
// Internal stuff - only touch if you know what you're doing
|
||||||
'BANNED': 0,
|
bcryptRounds: 10,
|
||||||
'RESTRICTED': 1,
|
roleMap: {
|
||||||
'USER': 2,
|
"BANNED": 0,
|
||||||
'BOT': 3,
|
"RESTRICTED": 1,
|
||||||
'ADMIN': 4
|
"USER": 2,
|
||||||
},
|
"BOT": 3,
|
||||||
gatewayStillNotConnectedTimeoutMS: 15*1000
|
"ADMIN": 4
|
||||||
};
|
},
|
||||||
|
gatewayStillNotConnectedTimeoutMS: 15*1000,
|
||||||
|
gatewayMaxStringPayloadLength:
|
||||||
|
};
|
||||||
|
|
114
index.js
114
index.js
|
@ -1,58 +1,58 @@
|
||||||
const config = require('./config');
|
const config = require("./config");
|
||||||
const apiRoute = require('./api/v1');
|
const apiRoute = require("./api/v1");
|
||||||
|
|
||||||
const express = require('express');
|
const express = require("express");
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require("cookie-parser");
|
||||||
const cors = require('cors')
|
const cors = require("cors");
|
||||||
const http = require('http');
|
const http = require("http");
|
||||||
|
|
||||||
const { authenticateEndpoint } = require('./api/v1/authfunctions');
|
const { authenticateEndpoint } = require("./api/v1/authfunctions");
|
||||||
const GatewayServer = require('./api/v1/gateway/index');
|
const GatewayServer = require("./api/v1/gateway/index");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpServer = http.createServer(app);
|
const httpServer = http.createServer(app);
|
||||||
|
|
||||||
const gateway = new GatewayServer(httpServer);
|
const gateway = new GatewayServer(httpServer);
|
||||||
|
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: function (origin, callback) {
|
origin: function (origin, callback) {
|
||||||
if (config.corsAllowList.indexOf(origin) !== -1 || !origin) {
|
if (config.corsAllowList.indexOf(origin) !== -1 || !origin) {
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
} else {
|
} else {
|
||||||
callback(new Error('Not allowed by CORS'));
|
callback(new Error("Not allowed by CORS"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
credentials: true,
|
credentials: true,
|
||||||
optionsSuccessStatus: 200
|
optionsSuccessStatus: 200
|
||||||
}));
|
}));
|
||||||
app.use('/api/v1', apiRoute);
|
app.use("/api/v1", apiRoute);
|
||||||
app.use(express.static('app'));
|
app.use(express.static("app"));
|
||||||
|
|
||||||
app.get('/', authenticateEndpoint((req, res, user) => {
|
app.get("/", authenticateEndpoint((req, res) => {
|
||||||
res.redirect('/app.html');
|
res.redirect("/app.html");
|
||||||
}, '/auth.html'));
|
}, "/auth.html"));
|
||||||
|
|
||||||
app.get('/admin', (req, res) => {
|
app.get("/admin", (req, res) => {
|
||||||
res.send('Keanu chungus wholesome 100 reddit moment 😀i beat up a kid that said minecraft bad 😂and my doggo bit him so i gave him snaccos😉 and we watched pewdiepie together while in elon musk’s cyber truck 😳talking about how superior reddit memers are : “haha emojis bad” 😲i said and keanu reeves came outta nowhere and said “this is wholesome 100, updoot this wholesome boy” 😗so i got alot of updoots and edit: thanks for the gold kind stranger😣. but the kind stranger revealed himself to be baby yoda eating chiccy nuggies😨 and drinking choccy milk😎 so we went to the cinema to see our (communism funny) favorite movies avengers endgame😆 but then thor played fortnite and fortnite bad😡, so then i said “reality is often dissappointing” and then baby yoda replied r/unexpectedthanos and i replied by r/expectedthanos😖 for balance and then danny devito came to pick us up from the cinema😩 and all the insta normies and gay mods stood watching😵 ,as we,superior redditors went home with danny devito to suck on his magnum dong😫 but i said no homo and started sucking,not like those gay mods😮,then the next morning we woke up to MrBeast telling us to plant 69420 million trees😌, me, baby yoda and danny said nice, and then on our way to plant 69420 million trees😊 (nice) we saw a kid doing a tiktok so keanu reeves appeared and said “we have a kid to burn” and i replied “you’re breathtaking”😄 so i said “i need a weapon” and baby yoda gave me an RPG so i blew the kid (DESTRUCTION 100)😎 and posted it on r/memes and r/dankmemes and r/pewdiepiesubmissions and got 1000000000 updoots😘,i’m sure pewds will give me a big pp, then we shat on emoji users😂😂 and started dreaming about girls that will never like me😢 and posted a lie on r/teenagers about how i got a GF after my doggo died by the hands of fortnite players😳 so i exploited his death for updoots😜, but i watched the sunset with the wholesome gang😁 (keanu,danny,Mrbeast, pewds, spongebob,stefan karl , bob ross, steve irwin, baby yoda and other artists that reddit exploits them) [Everyone liked that] WHOLESOME 100 REDDIT 100🤡');
|
res.send("Keanu chungus wholesome 100 reddit moment 😀i beat up a kid that said minecraft bad 😂and my doggo bit him so i gave him snaccos😉 and we watched pewdiepie together while in elon musk’s cyber truck 😳talking about how superior reddit memers are : “haha emojis bad” 😲i said and keanu reeves came outta nowhere and said “this is wholesome 100, updoot this wholesome boy” 😗so i got alot of updoots and edit: thanks for the gold kind stranger😣. but the kind stranger revealed himself to be baby yoda eating chiccy nuggies😨 and drinking choccy milk😎 so we went to the cinema to see our (communism funny) favorite movies avengers endgame😆 but then thor played fortnite and fortnite bad😡, so then i said “reality is often dissappointing” and then baby yoda replied r/unexpectedthanos and i replied by r/expectedthanos😖 for balance and then danny devito came to pick us up from the cinema😩 and all the insta normies and gay mods stood watching😵 ,as we,superior redditors went home with danny devito to suck on his magnum dong😫 but i said no homo and started sucking,not like those gay mods😮,then the next morning we woke up to MrBeast telling us to plant 69420 million trees😌, me, baby yoda and danny said nice, and then on our way to plant 69420 million trees😊 (nice) we saw a kid doing a tiktok so keanu reeves appeared and said “we have a kid to burn” and i replied “you’re breathtaking”😄 so i said “i need a weapon” and baby yoda gave me an RPG so i blew the kid (DESTRUCTION 100)😎 and posted it on r/memes and r/dankmemes and r/pewdiepiesubmissions and got 1000000000 updoots😘,i’m sure pewds will give me a big pp, then we shat on emoji users😂😂 and started dreaming about girls that will never like me😢 and posted a lie on r/teenagers about how i got a GF after my doggo died by the hands of fortnite players😳 so i exploited his death for updoots😜, but i watched the sunset with the wholesome gang😁 (keanu,danny,Mrbeast, pewds, spongebob,stefan karl , bob ross, steve irwin, baby yoda and other artists that reddit exploits them) [Everyone liked that] WHOLESOME 100 REDDIT 100🤡");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
console.error('[E] Internal server error', err);
|
console.error("[E] Internal server error", err);
|
||||||
res.status(500).json({ error: true, status: 500, message: 'ERR_INTERNAL_SERVER_ERROR' });
|
res.status(500).json({ error: true, status: 500, message: "ERR_INTERNAL_SERVER_ERROR" });
|
||||||
});
|
});
|
||||||
|
|
||||||
const onServerClosing = (evt) => {
|
const onServerClosing = () => {
|
||||||
gateway.notifyClientsOfUpdate('exit');
|
gateway.notifyClientsOfUpdate("exit");
|
||||||
process.exit();
|
process.exit();
|
||||||
};
|
};
|
||||||
|
|
||||||
['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].forEach((eventType) => {
|
["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "uncaughtException", "SIGTERM"].forEach((eventType) => {
|
||||||
process.on(eventType, onServerClosing.bind(null, eventType));
|
process.on(eventType, onServerClosing.bind(null, eventType));
|
||||||
})
|
});
|
||||||
|
|
||||||
httpServer.listen(config.ports.mainServerPort, () => {
|
httpServer.listen(config.ports.mainServerPort, () => {
|
||||||
console.log(`[*] [server] Main server is listening on port ${config.ports.mainServerPort}`);
|
console.log(`[*] [server] Main server is listening on port ${config.ports.mainServerPort}`);
|
||||||
});
|
});
|
|
@ -1,142 +0,0 @@
|
||||||
const Client = require('./index');
|
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
const LISTEN_ON = '5fcbf598b39160080e797ad6';
|
|
||||||
const PREFIX = '::';
|
|
||||||
const ADMIN_ID = '5fc828ea4e96e00725c17fd7';
|
|
||||||
|
|
||||||
const joined = [];
|
|
||||||
const selected = [];
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/17619741/randomly-generating-unique-number-in-pairs-multiple-times
|
|
||||||
|
|
||||||
const randomPairs = (players) => {
|
|
||||||
const pairs = [];
|
|
||||||
while(players.length) {
|
|
||||||
pairs.push([
|
|
||||||
pluckRandomElement(players),
|
|
||||||
pluckRandomElement(players)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return pairs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pluckRandomElement = (array) => {
|
|
||||||
const i = randomInt(array.length);
|
|
||||||
return array.splice(i, 1)[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const randomInt = (limit) => {
|
|
||||||
return Math.floor(Math.random() * limit);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRandomUser = (self, count=0) => {
|
|
||||||
if (count > 3) return joined[0];
|
|
||||||
count++;
|
|
||||||
|
|
||||||
let final;
|
|
||||||
|
|
||||||
let chosen = joined[Math.floor(Math.random() * joined.length)];
|
|
||||||
final = chosen;
|
|
||||||
if (chosen._id === self._id) return final = getRandomUser(self, count);
|
|
||||||
if ((selected.indexOf(chosen)) !== 1) return final = getRandomUser(self, count);
|
|
||||||
if (!final) return final = getRandomUser(self, count);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return final || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const main = async () => {
|
|
||||||
const client = new Client('https://b.hippoz.xyz', {
|
|
||||||
throwErrors: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await fetch(`${client.url}/api/v1/users/token/create`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: 'D',
|
|
||||||
password: 'D'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const json = await res.json();
|
|
||||||
|
|
||||||
if (!res.ok || json.error) throw new Error('Failed to generate token from API endpoint');
|
|
||||||
|
|
||||||
await client.setToken(json.token);
|
|
||||||
await client.gatewayConnect();
|
|
||||||
|
|
||||||
client.gateway.on('connect', () => {
|
|
||||||
const category = client.gateway.subscribeToCategoryChat(LISTEN_ON);
|
|
||||||
|
|
||||||
client.gateway.on('message', (e) => {
|
|
||||||
if (e.author._id === client.user._id) return;
|
|
||||||
if (!e.content.startsWith(PREFIX)) return;
|
|
||||||
if (e.category._id !== category) return;
|
|
||||||
|
|
||||||
const cmdString = e.content.substring(PREFIX.length);
|
|
||||||
const cmdFull = cmdString.split(' ');
|
|
||||||
const cmd = cmdFull[0] || 'NO_CMD';
|
|
||||||
|
|
||||||
console.log(cmdFull);
|
|
||||||
|
|
||||||
switch (cmd) {
|
|
||||||
case 'join': {
|
|
||||||
const existing = joined.findIndex((o) => {
|
|
||||||
return o._id === e.author._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing !== -1) {
|
|
||||||
client.gateway.sendMessage(category, 'Already joined', {
|
|
||||||
nickAuthor: { username: 'Error' },
|
|
||||||
destUser: { _id: e.author._id }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
joined.push(e.author);
|
|
||||||
console.log(`[*] User joined`, e.author);
|
|
||||||
client.gateway.sendMessage(category, `${e.author.username} joined!`, {
|
|
||||||
nickAuthor: { username: 'New join!' }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'roll': {
|
|
||||||
if (e.author._id !== ADMIN_ID) {
|
|
||||||
client.gateway.sendMessage(category, 'Access denied', {
|
|
||||||
nickAuthor: { username: 'Error' },
|
|
||||||
destUser: { _id: e.author._id }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
client.gateway.sendMessage(category, 'Rolling...', {
|
|
||||||
nickAuthor: { username: 'Woo' }
|
|
||||||
});
|
|
||||||
|
|
||||||
const pairs = randomPairs(joined);
|
|
||||||
|
|
||||||
for (const pair of pairs) {
|
|
||||||
const p1 = pair[0];
|
|
||||||
const p2 = pair[1];
|
|
||||||
if (!p1 || !p2) continue;
|
|
||||||
for (const player of pair) {
|
|
||||||
client.gateway.sendMessage(category, `${p1.username} with ${p2.username}`, {
|
|
||||||
nickAuthor: { username: 'Your result' },
|
|
||||||
destUser: { _id: player._id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
main();
|
|
|
@ -1,22 +1,22 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require("mongoose");
|
||||||
const Post = require('./Post');
|
const Post = require("./Post");
|
||||||
const User = require('./User');
|
const User = require("./User");
|
||||||
|
|
||||||
const categorySchema = new mongoose.Schema({
|
const categorySchema = new mongoose.Schema({
|
||||||
title: String,
|
title: String,
|
||||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
|
creator: {type: mongoose.Schema.Types.ObjectId, ref: "User"},
|
||||||
posts: [Post.schema]
|
posts: [Post.schema]
|
||||||
});
|
});
|
||||||
|
|
||||||
categorySchema.method('getPublicObject', function() {
|
categorySchema.method("getPublicObject", function() {
|
||||||
return {
|
return {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
creator: this.populate('creator', User.getPulicFields()).creator,
|
creator: this.populate("creator", User.getPulicFields()).creator,
|
||||||
posts: this.posts,
|
posts: this.posts,
|
||||||
_id: this._id
|
_id: this._id
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const Category = mongoose.model('Category', categorySchema);
|
const Category = mongoose.model("Category", categorySchema);
|
||||||
|
|
||||||
module.exports = Category;
|
module.exports = Category;
|
|
@ -1,10 +1,10 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
const Post = mongoose.model('Post', {
|
const Post = mongoose.model("Post", {
|
||||||
title: String,
|
title: String,
|
||||||
body: String,
|
body: String,
|
||||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
|
creator: {type: mongoose.Schema.Types.ObjectId, ref: "User"},
|
||||||
categoryId: {type: mongoose.Schema.Types.ObjectId, ref: 'Category'}
|
categoryId: {type: mongoose.Schema.Types.ObjectId, ref: "Category"}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = Post;
|
module.exports = Post;
|
146
models/User.js
146
models/User.js
|
@ -1,74 +1,74 @@
|
||||||
const config = require('../config');
|
const config = require("../config");
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
const userSchema = new mongoose.Schema({
|
const userSchema = new mongoose.Schema({
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
email: String,
|
email: String,
|
||||||
role: String,
|
role: String,
|
||||||
color: String
|
color: String
|
||||||
});
|
});
|
||||||
|
|
||||||
userSchema.method('getPublicObject', function() {
|
userSchema.method("getPublicObject", function() {
|
||||||
return {
|
return {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
permissionLevel: config.roleMap[this.role],
|
permissionLevel: config.roleMap[this.role],
|
||||||
role: this.role,
|
role: this.role,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
_id: this._id
|
_id: this._id
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
userSchema.method('getFullObject', function() {
|
userSchema.method("getFullObject", function() {
|
||||||
return {
|
return {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
permissionLevel: config.roleMap[this.role],
|
permissionLevel: config.roleMap[this.role],
|
||||||
role: this.role,
|
role: this.role,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
_id: this._id
|
_id: this._id
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const User = mongoose.model('User', userSchema);
|
const User = mongoose.model("User", userSchema);
|
||||||
|
|
||||||
// NOTE(hippoz): These are all actually material design colors, taken from https://material-ui.com/customization/color/#playground
|
// NOTE(hippoz): These are all actually material design colors, taken from https://material-ui.com/customization/color/#playground
|
||||||
const colors = [
|
const colors = [
|
||||||
'#f44336',
|
"#f44336",
|
||||||
'#e91e63',
|
"#e91e63",
|
||||||
'#9c27b0',
|
"#9c27b0",
|
||||||
'#673ab7',
|
"#673ab7",
|
||||||
'#3f51b5',
|
"#3f51b5",
|
||||||
'#2196f3',
|
"#2196f3",
|
||||||
'#03a9f4',
|
"#03a9f4",
|
||||||
'#00bcd4',
|
"#00bcd4",
|
||||||
'#009688',
|
"#009688",
|
||||||
'#4caf50',
|
"#4caf50",
|
||||||
'#8bc34a',
|
"#8bc34a",
|
||||||
'#cddc39',
|
"#cddc39",
|
||||||
'#ffeb3b',
|
"#ffeb3b",
|
||||||
'#ffc107',
|
"#ffc107",
|
||||||
'#ff9800',
|
"#ff9800",
|
||||||
'#ff5722'
|
"#ff5722"
|
||||||
];
|
];
|
||||||
|
|
||||||
User.generateColorFromUsername = function(username) {
|
User.generateColorFromUsername = function(username) {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i in username) {
|
for (let i in username) {
|
||||||
sum += username.charCodeAt(i);
|
sum += username.charCodeAt(i);
|
||||||
}
|
}
|
||||||
const colorIndex = sum % colors.length;
|
const colorIndex = sum % colors.length;
|
||||||
return colors[colorIndex];
|
return colors[colorIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
User.findByUsername = async function(username) {
|
User.findByUsername = async function(username) {
|
||||||
return await User.findOne({ username }).exec();
|
return await User.findOne({ username }).exec();
|
||||||
};
|
};
|
||||||
|
|
||||||
User.getPulicFields = function() {
|
User.getPulicFields = function() {
|
||||||
return 'username role _id color';
|
return "username role _id color";
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = User;
|
module.exports = User;
|
897
package-lock.json
generated
897
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -19,6 +19,10 @@
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mongoose": "^5.10.0",
|
"mongoose": "^5.10.0",
|
||||||
"socket.io": "^3.0.1",
|
"socket.io": "^3.0.1",
|
||||||
"uuid": "^8.3.1"
|
"uuid": "^8.3.1",
|
||||||
|
"ws": "^7.4.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^7.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
secret.js
20
secret.js
|
@ -1,10 +1,10 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
jwtPrivateKey: 'KEY'
|
jwtPrivateKey: "KEY"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set default values
|
// Set default values
|
||||||
// You shouldn't need to touch this for configuring this
|
// You shouldn't need to touch this for configuring this
|
||||||
if (module.exports.jwtPrivateKey === 'KEY') {
|
if (module.exports.jwtPrivateKey === "KEY") {
|
||||||
console.error('[*] [config] jwtPrivateKey was not specified in secret.js. A randomly generated private key will be used instead');
|
console.error("[*] [config] jwtPrivateKey was not specified in secret.js. A randomly generated private key will be used instead");
|
||||||
module.exports.jwtPrivateKey = require('crypto').randomBytes(129).toString('base64');
|
module.exports.jwtPrivateKey = require("crypto").randomBytes(129).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue