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,19 +1,19 @@
|
|||
const User = require('../../models/User');
|
||||
const secret = require('../../secret');
|
||||
const config = require('../../config');
|
||||
const User = require("../../models/User");
|
||||
const secret = require("../../secret");
|
||||
const config = require("../../config");
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
const redirect = (res, status=401, url=undefined) => {
|
||||
if (!url) {
|
||||
res.status(status).json({
|
||||
error: true,
|
||||
message: 'ERROR_ACCESS_DENIED'
|
||||
message: "ERROR_ACCESS_DENIED"
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.redirect(url);
|
||||
}
|
||||
};
|
||||
|
||||
function authenticateEndpoint(callback, url=undefined, minPermissionLevel=config.roleMap.RESTRICTED) {
|
||||
return (req, res) => {
|
||||
|
@ -31,7 +31,7 @@ function authenticateEndpoint(callback, url=undefined, minPermissionLevel=config
|
|||
|
||||
if (!data) {
|
||||
redirect(res, 401, url);
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.username) {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const User = require('../../models/User');
|
||||
const Category = require('../../models/Category');
|
||||
const Post = require('../../models/Post');
|
||||
const config = require('../../config');
|
||||
const User = require("../../models/User");
|
||||
const Category = require("../../models/Category");
|
||||
const Post = require("../../models/Post");
|
||||
const config = require("../../config");
|
||||
|
||||
const { authenticateEndpoint } = require('./authfunctions');
|
||||
const { authenticateEndpoint } = require("./authfunctions");
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const { body, param, validationResult } = require('express-validator');
|
||||
const express = require('express');
|
||||
const mongoose = require("mongoose");
|
||||
const { body, param, validationResult } = require("express-validator");
|
||||
const express = require("express");
|
||||
|
||||
const app = express.Router();
|
||||
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
||||
|
@ -19,13 +19,13 @@ const createLimiter = rateLimit({
|
|||
max: 10,
|
||||
});
|
||||
|
||||
app.post('/category/create', [
|
||||
app.post("/category/create", [
|
||||
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) => {
|
||||
const errors = validationResult(req);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -38,20 +38,20 @@ app.post('/category/create', [
|
|||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_CATEGORY_CREATED',
|
||||
message: "SUCCESS_CATEGORY_CREATED",
|
||||
category: category.getPublicObject()
|
||||
});
|
||||
}, undefined, config.roleMap.USER));
|
||||
|
||||
app.post('/post/create', [
|
||||
app.post("/post/create", [
|
||||
createLimiter,
|
||||
body('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }),
|
||||
body('title').not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
|
||||
body('body').not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
|
||||
body("category").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }),
|
||||
body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
|
||||
body("body").not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
|
||||
], authenticateEndpoint(async (req, res, user) => {
|
||||
const errors = validationResult(req);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -74,46 +74,46 @@ app.post('/post/create', [
|
|||
if (r.n < 1) {
|
||||
res.status(404).json({
|
||||
error: true,
|
||||
message: 'ERROR_CATEGORY_NOT_FOUND'
|
||||
message: "ERROR_CATEGORY_NOT_FOUND"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_POST_CREATED',
|
||||
message: "SUCCESS_POST_CREATED",
|
||||
post: {
|
||||
_id: post._id
|
||||
}
|
||||
});
|
||||
}, undefined, config.roleMap.USER));
|
||||
|
||||
app.get('/category/:category/info', [
|
||||
param('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||
], authenticateEndpoint(async (req, res, user) => {
|
||||
app.get("/category/:category/info", [
|
||||
param("category").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||
], authenticateEndpoint(async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
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) {
|
||||
res.status(404).json({
|
||||
error: true,
|
||||
message: 'ERROR_CATEGORY_NOT_FOUND'
|
||||
message: "ERROR_CATEGORY_NOT_FOUND"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_CATEGORY_DATA_FETCHED',
|
||||
message: "SUCCESS_CATEGORY_DATA_FETCHED",
|
||||
category: category.getPublicObject(),
|
||||
userInfo: {
|
||||
userListLimit: 50,
|
||||
|
@ -122,18 +122,18 @@ app.get('/category/:category/info', [
|
|||
});
|
||||
}, 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);
|
||||
if (!Number.isInteger(count)) {
|
||||
count = 10;
|
||||
}
|
||||
|
||||
// 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({
|
||||
error: false,
|
||||
message: 'SUCCESS_CATEGORY_LIST_FETCHED',
|
||||
message: "SUCCESS_CATEGORY_LIST_FETCHED",
|
||||
categories
|
||||
});
|
||||
}, undefined, config.roleMap.USER));
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
const User = require('../../../models/User');
|
||||
const secret = require('../../../secret');
|
||||
const config = require('../../../config');
|
||||
const Category = require('../../../models/Category');
|
||||
const RateLimiter = require('./ratelimiter');
|
||||
const User = require("../../../models/User");
|
||||
const secret = require("../../../secret");
|
||||
const config = require("../../../config");
|
||||
const Category = require("../../../models/Category");
|
||||
const RateLimiter = require("./ratelimiter");
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const siolib = require('socket.io');
|
||||
const uuid = require('uuid');
|
||||
const jwt = require("jsonwebtoken");
|
||||
const siolib = require("socket.io");
|
||||
const uuid = require("uuid");
|
||||
|
||||
class GatewayServer {
|
||||
constructor(httpServer) {
|
||||
this._io = siolib(httpServer);
|
||||
this._gateway = this._io.of('/gateway');
|
||||
this._gateway = this._io.of("/gateway");
|
||||
this.rateLimiter = new RateLimiter({
|
||||
points: 5,
|
||||
time: 1000,
|
||||
|
@ -19,15 +19,15 @@ class GatewayServer {
|
|||
});
|
||||
this.eventSetup();
|
||||
|
||||
this._commandPrefix = '/';
|
||||
this._commandPrefix = "/";
|
||||
}
|
||||
}
|
||||
|
||||
GatewayServer.prototype._sendSystemMessage = function(socket, message, category) {
|
||||
const messageObject = {
|
||||
author: {
|
||||
username: '__SYSTEM',
|
||||
_id: '5fc69864f15a7c5e504c9a1f'
|
||||
username: "__SYSTEM",
|
||||
_id: "5fc69864f15a7c5e504c9a1f"
|
||||
},
|
||||
category: {
|
||||
title: category.title,
|
||||
|
@ -37,78 +37,78 @@ GatewayServer.prototype._sendSystemMessage = function(socket, message, category)
|
|||
_id: uuid.v4()
|
||||
};
|
||||
|
||||
socket.emit('message', messageObject);
|
||||
socket.emit("message", messageObject);
|
||||
};
|
||||
|
||||
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) {
|
||||
const content = message.content;
|
||||
const fullCommandString = content.slice(this._commandPrefix.length, content.length);
|
||||
const fullCommand = fullCommandString.split(' ');
|
||||
const command = fullCommand[0] || 'INVALID_COMMAND';
|
||||
const fullCommand = fullCommandString.split(" ");
|
||||
const command = fullCommand[0] || "INVALID_COMMAND";
|
||||
const args = fullCommand.length - 1;
|
||||
|
||||
switch (command) {
|
||||
case 'INVALID_COMMAND': {
|
||||
this._sendSystemMessage(socket, 'Invalid command.', message.category);
|
||||
case "INVALID_COMMAND": {
|
||||
this._sendSystemMessage(socket, "Invalid command.", message.category);
|
||||
break;
|
||||
}
|
||||
case 'admin/fr': {
|
||||
case "admin/fr": {
|
||||
if (args === 1) {
|
||||
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);
|
||||
this._sendSystemMessage(socket, "how about no", message.category);
|
||||
}
|
||||
} else {
|
||||
this._sendSystemMessage(socket, 'Invalid number of arguments.', message.category);
|
||||
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'admin/fru': {
|
||||
case "admin/fru": {
|
||||
if (args === 1) {
|
||||
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
|
||||
const user = await this._findSocketInRoom(message.category._id, fullCommand[1]);
|
||||
if (!user) {
|
||||
this._sendSystemMessage(socket, 'User not found.', message.category);
|
||||
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);
|
||||
this._sendSystemMessage(socket, "how about no", message.category);
|
||||
}
|
||||
} else {
|
||||
this._sendSystemMessage(socket, 'Invalid number of arguments.', message.category);
|
||||
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this._sendSystemMessage(socket, 'That command does not exist.', message.category);
|
||||
this._sendSystemMessage(socket, "That command does not exist.", message.category);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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.disconnect();
|
||||
socket.disconnect(true);
|
||||
callback(new Error('ERR_GATEWAY_AUTH_FAIL'));
|
||||
callback(new Error("ERR_GATEWAY_AUTH_FAIL"));
|
||||
};
|
||||
|
||||
GatewayServer.prototype.eventSetup = function() {
|
||||
this._gateway.use((socket, callback) => {
|
||||
console.log('[*] [gateway] [handshake] User authentication attempt');
|
||||
console.log("[*] [gateway] [handshake] User authentication attempt");
|
||||
socket.isConnected = false;
|
||||
|
||||
setTimeout(() => {
|
||||
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(true);
|
||||
}, config.gatewayStillNotConnectedTimeoutMS);
|
||||
|
@ -117,7 +117,7 @@ GatewayServer.prototype.eventSetup = function() {
|
|||
const token = socket.handshake.query.token;
|
||||
|
||||
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;
|
||||
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...`);
|
||||
|
||||
socket.emit('hello', {
|
||||
socket.emit("hello", {
|
||||
gatewayStillNotConnectedTimeoutMS: config.gatewayStillNotConnectedTimeoutMS,
|
||||
resolvedUser: {
|
||||
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!`);
|
||||
socket.isConnected = true;
|
||||
|
||||
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;
|
||||
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;
|
||||
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?
|
||||
console.log(`[E] [gateway] Rate limiting ${socket.user.username}`);
|
||||
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
|
||||
const categoryTitle = socket.joinedCategories[category._id];
|
||||
if (!categoryTitle || !(typeof categoryTitle === 'string')) return;
|
||||
if (!categoryTitle || !(typeof categoryTitle === "string")) return;
|
||||
|
||||
let messageObject = {
|
||||
author: {
|
||||
|
@ -197,14 +197,14 @@ GatewayServer.prototype.eventSetup = function() {
|
|||
_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) {
|
||||
messageObject = {
|
||||
nickAuthor: {
|
||||
username: nickAuthor.username
|
||||
},
|
||||
...messageObject
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,22 +213,22 @@ GatewayServer.prototype.eventSetup = function() {
|
|||
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);
|
||||
if (!user) return;
|
||||
|
||||
this._gateway.in(user.user.sid).emit('message', messageObject);
|
||||
this._gateway.in(user.user.sid).emit("message", messageObject);
|
||||
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;
|
||||
try {
|
||||
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
|
||||
const category = await Category.findById(v);
|
||||
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...`);
|
||||
|
||||
const upd = await this._generateClientListUpdateObject(v, category.title);
|
||||
this._gateway.in(v).emit('clientListUpdate', upd);
|
||||
this._gateway.in(v).emit("clientListUpdate", upd);
|
||||
}
|
||||
}
|
||||
} 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...`);
|
||||
const rooms = socket.rooms;
|
||||
rooms.forEach(async (room) => {
|
||||
// Socket io automatically adds a user to a room with their own id
|
||||
if (room === socket.id) return;
|
||||
|
||||
const categoryTitle = socket.joinedCategories[room] || 'UNKNOWN';
|
||||
const categoryTitle = socket.joinedCategories[room] || "UNKNOWN";
|
||||
await socket.leave(room);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle='UNKNOWN') {
|
||||
GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle="UNKNOWN") {
|
||||
const clientList = await this._getSocketsInRoom(room);
|
||||
return {
|
||||
category: {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
const usersAPI = require('./users');
|
||||
const contentAPI = require('./content');
|
||||
const usersAPI = require("./users");
|
||||
const contentAPI = require("./content");
|
||||
|
||||
const express = require('express');
|
||||
const express = require("express");
|
||||
|
||||
const app = express.Router();
|
||||
|
||||
app.use('/users', usersAPI);
|
||||
app.use('/content', contentAPI);
|
||||
app.use("/users", usersAPI);
|
||||
app.use("/content", contentAPI);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
app.get("/", (req, res) => {
|
||||
// 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;
|
|
@ -1,15 +1,15 @@
|
|||
const User = require('../../models/User');
|
||||
const config = require('../../config');
|
||||
const secret = require('../../secret');
|
||||
const User = require("../../models/User");
|
||||
const config = require("../../config");
|
||||
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/)
|
||||
const bcrypt = require('bcrypt');
|
||||
const mongoose = require('mongoose');
|
||||
const { body, query, param, validationResult } = require('express-validator');
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require("bcrypt");
|
||||
const mongoose = require("mongoose");
|
||||
const { body, param, validationResult } = require("express-validator");
|
||||
const express = require("express");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
const app = express.Router();
|
||||
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
||||
|
@ -22,7 +22,7 @@ const createAccountLimiter = rateLimit({
|
|||
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;
|
||||
if (config.restrictions) {
|
||||
const restrictions = config.restrictions.signup;
|
||||
|
@ -33,22 +33,22 @@ app.get('/account/create/info', async (req, res) => {
|
|||
|
||||
res.json({
|
||||
error: false,
|
||||
message: 'SUCCESS_ACCOUNT_CREATE_INFO_FETCH',
|
||||
message: "SUCCESS_ACCOUNT_CREATE_INFO_FETCH",
|
||||
requiresSpecialCode: requiresCode
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/account/create', [
|
||||
app.post("/account/create", [
|
||||
createAccountLimiter,
|
||||
body('username').not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(),
|
||||
body('email').not().isEmpty().isEmail().normalizeEmail(),
|
||||
body('password').not().isEmpty().isLength({ min: 8, max: 128 }),
|
||||
body('specialCode').optional().isLength({ min: 12, max: 12 }).isAlphanumeric()
|
||||
body("username").not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(),
|
||||
body("email").not().isEmpty().isEmail().normalizeEmail(),
|
||||
body("password").not().isEmpty().isLength({ min: 8, max: 128 }),
|
||||
body("specialCode").optional().isLength({ min: 12, max: 12 }).isAlphanumeric()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -60,11 +60,11 @@ app.post('/account/create', [
|
|||
|
||||
if (passedSpecialCode && specialCode) {
|
||||
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;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
@ -74,13 +74,13 @@ app.post('/account/create', [
|
|||
|
||||
const existingUser = await User.findByUsername(username);
|
||||
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;
|
||||
}
|
||||
|
||||
const unhashedPassword = req.body.password;
|
||||
const email = req.body.email;
|
||||
const startingRole = 'USER';
|
||||
const startingRole = "USER";
|
||||
|
||||
const hashedPassword = await bcrypt.hash(unhashedPassword, config.bcryptRounds);
|
||||
|
||||
|
@ -94,28 +94,28 @@ app.post('/account/create', [
|
|||
|
||||
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({
|
||||
error: false,
|
||||
message: 'SUCCESS_USER_CREATED',
|
||||
message: "SUCCESS_USER_CREATED",
|
||||
user: userObject
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Internal server error', e);
|
||||
res.status(500).json({ error: true, message: 'INTERNAL_SERVER_ERROR' });
|
||||
console.error("Internal server error", e);
|
||||
res.status(500).json({ error: true, message: "INTERNAL_SERVER_ERROR" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/token/create', [
|
||||
app.post("/token/create", [
|
||||
createAccountLimiter,
|
||||
body('username').not().isEmpty().trim().isAlphanumeric(),
|
||||
body('password').not().isEmpty()
|
||||
body("username").not().isEmpty().trim().isAlphanumeric(),
|
||||
body("password").not().isEmpty()
|
||||
], async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -123,7 +123,7 @@ app.post('/token/create', [
|
|||
|
||||
const existingUser = await User.findByUsername(username);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -136,45 +136,45 @@ app.post('/token/create', [
|
|||
passwordCheck = false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: '3h' }, async (err, token) => {
|
||||
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: "3h" }, async (err, token) => {
|
||||
if (err) {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
message: 'INTERNAL_SERVER_ERROR'
|
||||
message: "INTERNAL_SERVER_ERROR"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Ugly fix for setting httponly cookies
|
||||
if (req.body.alsoSetCookie) {
|
||||
res.cookie('token', token, {
|
||||
res.cookie("token", token, {
|
||||
maxAge: 3 * 60 * 60 * 1000, httpOnly: true, domain: config.address,
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
error: false,
|
||||
message: 'SUCCESS_TOKEN_CREATED',
|
||||
message: "SUCCESS_TOKEN_CREATED",
|
||||
user: userObject,
|
||||
token
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/current/info', authenticateEndpoint(async (req, res, user) => {
|
||||
app.get("/current/info", authenticateEndpoint(async (req, res, user) => {
|
||||
const userObject = await user.getPublicObject();
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_USER_DATA_FETCHED',
|
||||
message: "SUCCESS_USER_DATA_FETCHED",
|
||||
user: {
|
||||
token: req.cookies.token, // TODO: Passing the token like this is *terribly* insecure
|
||||
...userObject
|
||||
|
@ -182,12 +182,12 @@ app.get('/current/info', authenticateEndpoint(async (req, res, user) => {
|
|||
});
|
||||
}, undefined, 0));
|
||||
|
||||
app.get('/user/:userid/info', [
|
||||
param('userid').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||
], authenticateEndpoint(async (req, res, user) => {
|
||||
app.get("/user/:userid/info", [
|
||||
param("userid").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||
], authenticateEndpoint(async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -195,7 +195,7 @@ app.get('/user/:userid/info', [
|
|||
if (!userid) {
|
||||
res.sendStatus(400).json({
|
||||
error: true,
|
||||
message: 'ERROR_REQUEST_INVALID_DATA'
|
||||
message: "ERROR_REQUEST_INVALID_DATA"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -203,13 +203,13 @@ app.get('/user/:userid/info', [
|
|||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_USER_DATA_FETCHED',
|
||||
message: "SUCCESS_USER_DATA_FETCHED",
|
||||
user: await otherUser.getPublicObject(),
|
||||
});
|
||||
}, undefined, config.roleMap.USER));
|
||||
|
||||
app.post('/browser/token/clear', authenticateEndpoint((req, res, user) => {
|
||||
res.clearCookie('token');
|
||||
app.post("/browser/token/clear", authenticateEndpoint((req, res) => {
|
||||
res.clearCookie("token");
|
||||
res.sendStatus(200);
|
||||
}));
|
||||
|
||||
|
|
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 };
|
21
config.js
21
config.js
|
@ -2,21 +2,24 @@ module.exports = {
|
|||
ports: {
|
||||
mainServerPort: 3005,
|
||||
},
|
||||
address: 'localhost',
|
||||
address: "localhost",
|
||||
//restrictions: {
|
||||
// signup: {
|
||||
// specialCode: ''
|
||||
// }
|
||||
//},
|
||||
corsAllowList: [ 'localhost' ],
|
||||
mongoUrl: 'mongodb://localhost:27017/app',
|
||||
corsAllowList: [ "http://localhost:3000", "http://localhost:3005" ],
|
||||
mongoUrl: "mongodb://192.168.0.105:27017/app",
|
||||
|
||||
// Internal stuff - only touch if you know what you're doing
|
||||
bcryptRounds: 10,
|
||||
roleMap: {
|
||||
'BANNED': 0,
|
||||
'RESTRICTED': 1,
|
||||
'USER': 2,
|
||||
'BOT': 3,
|
||||
'ADMIN': 4
|
||||
"BANNED": 0,
|
||||
"RESTRICTED": 1,
|
||||
"USER": 2,
|
||||
"BOT": 3,
|
||||
"ADMIN": 4
|
||||
},
|
||||
gatewayStillNotConnectedTimeoutMS: 15*1000
|
||||
gatewayStillNotConnectedTimeoutMS: 15*1000,
|
||||
gatewayMaxStringPayloadLength:
|
||||
};
|
||||
|
|
44
index.js
44
index.js
|
@ -1,13 +1,13 @@
|
|||
const config = require('./config');
|
||||
const apiRoute = require('./api/v1');
|
||||
const config = require("./config");
|
||||
const apiRoute = require("./api/v1");
|
||||
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const cors = require('cors')
|
||||
const http = require('http');
|
||||
const express = require("express");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const cors = require("cors");
|
||||
const http = require("http");
|
||||
|
||||
const { authenticateEndpoint } = require('./api/v1/authfunctions');
|
||||
const GatewayServer = require('./api/v1/gateway/index');
|
||||
const { authenticateEndpoint } = require("./api/v1/authfunctions");
|
||||
const GatewayServer = require("./api/v1/gateway/index");
|
||||
|
||||
const app = express();
|
||||
const httpServer = http.createServer(app);
|
||||
|
@ -22,36 +22,36 @@ app.use(cors({
|
|||
if (config.corsAllowList.indexOf(origin) !== -1 || !origin) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200
|
||||
}));
|
||||
app.use('/api/v1', apiRoute);
|
||||
app.use(express.static('app'));
|
||||
app.use("/api/v1", apiRoute);
|
||||
app.use(express.static("app"));
|
||||
|
||||
app.get('/', authenticateEndpoint((req, res, user) => {
|
||||
res.redirect('/app.html');
|
||||
}, '/auth.html'));
|
||||
app.get("/", authenticateEndpoint((req, res) => {
|
||||
res.redirect("/app.html");
|
||||
}, "/auth.html"));
|
||||
|
||||
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🤡');
|
||||
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🤡");
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('[E] Internal server error', err);
|
||||
res.status(500).json({ error: true, status: 500, message: 'ERR_INTERNAL_SERVER_ERROR' });
|
||||
console.error("[E] Internal server error", err);
|
||||
res.status(500).json({ error: true, status: 500, message: "ERR_INTERNAL_SERVER_ERROR" });
|
||||
});
|
||||
|
||||
const onServerClosing = (evt) => {
|
||||
gateway.notifyClientsOfUpdate('exit');
|
||||
const onServerClosing = () => {
|
||||
gateway.notifyClientsOfUpdate("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));
|
||||
})
|
||||
});
|
||||
|
||||
httpServer.listen(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 Post = require('./Post');
|
||||
const User = require('./User');
|
||||
const mongoose = require("mongoose");
|
||||
const Post = require("./Post");
|
||||
const User = require("./User");
|
||||
|
||||
const categorySchema = new mongoose.Schema({
|
||||
title: String,
|
||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
|
||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: "User"},
|
||||
posts: [Post.schema]
|
||||
});
|
||||
|
||||
categorySchema.method('getPublicObject', function() {
|
||||
categorySchema.method("getPublicObject", function() {
|
||||
return {
|
||||
title: this.title,
|
||||
creator: this.populate('creator', User.getPulicFields()).creator,
|
||||
creator: this.populate("creator", User.getPulicFields()).creator,
|
||||
posts: this.posts,
|
||||
_id: this._id
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Category = mongoose.model('Category', categorySchema);
|
||||
const Category = mongoose.model("Category", categorySchema);
|
||||
|
||||
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,
|
||||
body: String,
|
||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
|
||||
categoryId: {type: mongoose.Schema.Types.ObjectId, ref: 'Category'}
|
||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: "User"},
|
||||
categoryId: {type: mongoose.Schema.Types.ObjectId, ref: "Category"}
|
||||
});
|
||||
|
||||
module.exports = Post;
|
|
@ -1,6 +1,6 @@
|
|||
const config = require('../config');
|
||||
const config = require("../config");
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
username: String,
|
||||
|
@ -10,17 +10,17 @@ const userSchema = new mongoose.Schema({
|
|||
color: String
|
||||
});
|
||||
|
||||
userSchema.method('getPublicObject', function() {
|
||||
userSchema.method("getPublicObject", function() {
|
||||
return {
|
||||
username: this.username,
|
||||
permissionLevel: config.roleMap[this.role],
|
||||
role: this.role,
|
||||
color: this.color,
|
||||
_id: this._id
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
userSchema.method('getFullObject', function() {
|
||||
userSchema.method("getFullObject", function() {
|
||||
return {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
|
@ -29,29 +29,29 @@ userSchema.method('getFullObject', function() {
|
|||
role: this.role,
|
||||
color: this.color,
|
||||
_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
|
||||
const colors = [
|
||||
'#f44336',
|
||||
'#e91e63',
|
||||
'#9c27b0',
|
||||
'#673ab7',
|
||||
'#3f51b5',
|
||||
'#2196f3',
|
||||
'#03a9f4',
|
||||
'#00bcd4',
|
||||
'#009688',
|
||||
'#4caf50',
|
||||
'#8bc34a',
|
||||
'#cddc39',
|
||||
'#ffeb3b',
|
||||
'#ffc107',
|
||||
'#ff9800',
|
||||
'#ff5722'
|
||||
"#f44336",
|
||||
"#e91e63",
|
||||
"#9c27b0",
|
||||
"#673ab7",
|
||||
"#3f51b5",
|
||||
"#2196f3",
|
||||
"#03a9f4",
|
||||
"#00bcd4",
|
||||
"#009688",
|
||||
"#4caf50",
|
||||
"#8bc34a",
|
||||
"#cddc39",
|
||||
"#ffeb3b",
|
||||
"#ffc107",
|
||||
"#ff9800",
|
||||
"#ff5722"
|
||||
];
|
||||
|
||||
User.generateColorFromUsername = function(username) {
|
||||
|
@ -68,7 +68,7 @@ User.findByUsername = async function(username) {
|
|||
};
|
||||
|
||||
User.getPulicFields = function() {
|
||||
return 'username role _id color';
|
||||
return "username role _id color";
|
||||
};
|
||||
|
||||
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",
|
||||
"mongoose": "^5.10.0",
|
||||
"socket.io": "^3.0.1",
|
||||
"uuid": "^8.3.1"
|
||||
"uuid": "^8.3.1",
|
||||
"ws": "^7.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.21.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
module.exports = {
|
||||
jwtPrivateKey: 'KEY'
|
||||
jwtPrivateKey: "KEY"
|
||||
};
|
||||
|
||||
// Set default values
|
||||
// You shouldn't need to touch this for configuring this
|
||||
if (module.exports.jwtPrivateKey === 'KEY') {
|
||||
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');
|
||||
if (module.exports.jwtPrivateKey === "KEY") {
|
||||
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");
|
||||
}
|
||||
|
|
Reference in a new issue