From 495bd71f3678f7b4930db6207fdafdff2432512c Mon Sep 17 00:00:00 2001 From: hippoz Date: Sat, 2 Oct 2021 22:50:12 +0300 Subject: [PATCH 1/4] feat: add message storage and fetching api --- brainlet/api/v1/content.js | 42 ++++++++++++++++++++++++++------ brainlet/api/v2/gateway/index.js | 15 ++++++++++-- brainlet/config.js | 4 +-- brainlet/models/Message.js | 12 +++++++++ 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 brainlet/models/Message.js diff --git a/brainlet/api/v1/content.js b/brainlet/api/v1/content.js index 1a01e4a..a73dcc4 100755 --- a/brainlet/api/v1/content.js +++ b/brainlet/api/v1/content.js @@ -1,6 +1,7 @@ const User = require("../../models/User"); const Channel = require("../../models/Channel"); const Post = require("../../models/Post"); +const Message = require("../../models/Message"); const config = require("../../config"); const { authenticateEndpoint } = require("./../../common/auth/authfunctions"); @@ -38,7 +39,7 @@ app.post("/channel/create", [ res.status(200).json({ error: false, - message: "SUCCESS_CATEGORY_CREATED", + message: "SUCCESS_CHANNEL_CREATED", channel: channel.getPublicObject() }); }, undefined, config.roleMap.USER)); @@ -76,7 +77,7 @@ app.post("/post/create", [ if (r.n < 1) { res.status(404).json({ error: true, - message: "ERROR_CATEGORY_NOT_FOUND" + message: "ERROR_CHANNEL_NOT_FOUND" }); return; } @@ -90,6 +91,33 @@ app.post("/post/create", [ }); }, undefined, config.roleMap.USER)); +app.get("/channel/:channel/messages", [ + param("channel").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() }); + return; + } + + let idSearch = {}; + if (req.query.before) { + idSearch = { _id: { $lt: req.query.before } }; + } + + const messages = await Message.find({ channel: req.params.channel, ...idSearch }) + .sort({ _id: -1 }) + .limit(50) + .select("-__v -channel") + .populate("author", "_id username"); + + res.status(200).json({ + error: false, + message: "SUCCESS_CHANNEL_MESSAGES_FETCHED", + channelMessages: messages + }); +}, undefined, config.roleMap.USER)); + app.get("/channel/:channel/info", [ param("channel").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }) ], authenticateEndpoint(async (req, res) => { @@ -99,23 +127,21 @@ app.get("/channel/:channel/info", [ return; } - const channelId = req.params.channel; - const channel = await Channel.findById(channelId).populate("posts.creator", User.getPulicFields()); + const channel = await Channel.findById(req.params.channel).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()); if (!channel) { res.status(404).json({ error: true, - message: "ERROR_CATEGORY_NOT_FOUND" + message: "ERROR_CHANNEL_NOT_FOUND" }); return; } res.status(200).json({ error: false, - message: "SUCCESS_CATEGORY_DATA_FETCHED", + message: "SUCCESS_CHANNEL_DATA_FETCHED", channel: channel.getPublicObject(), userInfo: { userListLimit: 50, @@ -135,7 +161,7 @@ app.get("/channel/list", authenticateEndpoint(async (req, res) => { res.status(200).json({ error: false, - message: "SUCCESS_CATEGORY_LIST_FETCHED", + message: "SUCCESS_CHANNEL_LIST_FETCHED", channels }); }, undefined, config.roleMap.USER)); diff --git a/brainlet/api/v2/gateway/index.js b/brainlet/api/v2/gateway/index.js index 6cb25fa..ca3f40a 100644 --- a/brainlet/api/v2/gateway/index.js +++ b/brainlet/api/v2/gateway/index.js @@ -5,6 +5,7 @@ const { policies, gatewayPingInterval, gatewayPingCheckInterval, clientFacingPin const { experiments } = require("../../../experiments"); const User = require("../../../models/User"); const Channel = require("../../../models/Channel"); +const Message = require("../../../models/Message"); const { parseMessage, packet } = require("./messageparser"); const { checkToken } = require("../../../common/auth/authfunctions"); @@ -20,10 +21,11 @@ const wsCloseCodes = { }; const attributes = { - PRESENCE_UPDATES: "PRESENCE_UPDATES" + PRESENCE_UPDATES: "PRESENCE_UPDATES", + SAVE_MESSAGES: "SAVE_MESSAGES" }; -const supportedAttributes = [attributes.PRESENCE_UPDATES]; +const supportedAttributes = [attributes.PRESENCE_UPDATES, attributes.SAVE_MESSAGES]; class GatewaySession { constructor() { @@ -243,6 +245,15 @@ class GatewayHandler { _id: uuid.v4() }); }); + + if (session.hasAttribute(attributes.SAVE_MESSAGES)) { + await Message.create({ + author: session.user._id, + channel: data.channel._id, + content: messageContent, + createdAt: new Date().getTime() + }); + } } } diff --git a/brainlet/config.js b/brainlet/config.js index 656dc88..d689a76 100755 --- a/brainlet/config.js +++ b/brainlet/config.js @@ -10,7 +10,6 @@ module.exports = { // "https://example.com" "http://localhost:3005", // Allow the server itself (provided it's listening on 3005) - //"http://localhost:3000" // Optionally allow the react app development server (which listens on 3000 by default) ], policies: { // Currently, policies apply to all users - no matter the role. @@ -19,7 +18,7 @@ module.exports = { allowAccountCreation: true, allowLogin: true, allowGatewayConnection: true, - perUserMaxGatewayConnections: 4, + perUserMaxGatewayConnections: 4 }, /* --- Adding a special code requirement for account creation @@ -36,6 +35,7 @@ module.exports = { gatewayPingInterval: 15000, gatewayPingCheckInterval: 4500, clientFacingPingInterval: 14750, + unsafeStoreMessages: false, bcryptRounds: 10, roleMap: { "BANNED": 0, diff --git a/brainlet/models/Message.js b/brainlet/models/Message.js new file mode 100644 index 0000000..e60a9f9 --- /dev/null +++ b/brainlet/models/Message.js @@ -0,0 +1,12 @@ +const mongoose = require("mongoose"); + +const messageSchema = new mongoose.Schema({ + author: {type: mongoose.Schema.Types.ObjectId, ref: "User"}, + channel: {type: mongoose.Schema.Types.ObjectId, ref: "Channel"}, + content: String, + createdAt: Number +}); + +const Message = mongoose.model("Message", messageSchema); + +module.exports = Message; \ No newline at end of file From 1008e68d54138005e64ed0c49bb1981d2ccbb758 Mon Sep 17 00:00:00 2001 From: hippoz Date: Sun, 3 Oct 2021 21:05:54 +0300 Subject: [PATCH 2/4] feat: add `allowSavingMessages` policy and remove SAVE_MESSAGES attribute, as well as change HELLO packet --- brainlet/api/v1/content.js | 10 ++++++++ brainlet/api/v2/gateway/index.js | 42 ++++++++++++++++++-------------- brainlet/config.js | 4 ++- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/brainlet/api/v1/content.js b/brainlet/api/v1/content.js index a73dcc4..03bae5b 100755 --- a/brainlet/api/v1/content.js +++ b/brainlet/api/v1/content.js @@ -94,6 +94,16 @@ app.post("/post/create", [ app.get("/channel/:channel/messages", [ param("channel").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }) ], authenticateEndpoint(async (req, res) => { + if (!config.policies.allowSavingMessages) { + // TODO: hack + res.status(200).json({ + error: false, + message: "SUCCESS_CHANNEL_MESSAGES_FETCHED", + channelMessages: [] + }); + return; + } + const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() }); diff --git a/brainlet/api/v2/gateway/index.js b/brainlet/api/v2/gateway/index.js index ca3f40a..3c3ba59 100644 --- a/brainlet/api/v2/gateway/index.js +++ b/brainlet/api/v2/gateway/index.js @@ -1,5 +1,6 @@ const websockets = require("ws"); -const uuid = require("uuid"); +const { v4 } = require("uuid"); +const mongoose = require("mongoose"); const { policies, gatewayPingInterval, gatewayPingCheckInterval, clientFacingPingInterval } = require("../../../config"); const { experiments } = require("../../../experiments"); @@ -18,21 +19,21 @@ const wsCloseCodes = { NOT_AUTHORIZED: [4006, "Not authorized"], FLOODING: [4007, "Flooding"], NO_PING: [4008, "No ping"], + UNSUPPORTED_ATTRIBUTE: [4009, "Unsupported attribute."], }; const attributes = { PRESENCE_UPDATES: "PRESENCE_UPDATES", - SAVE_MESSAGES: "SAVE_MESSAGES" }; -const supportedAttributes = [attributes.PRESENCE_UPDATES, attributes.SAVE_MESSAGES]; +const supportedAttributes = [attributes.PRESENCE_UPDATES]; class GatewaySession { constructor() { this.authenticated = false; this.user = null; this.token = null; - this.sessionId = uuid.v4(); + this.sessionId = v4(); this.attributes = []; // Specific to websocket sessions @@ -110,7 +111,7 @@ class GatewayHandler { const session = new GatewaySession(); session.setWebsocketClient(ws); - session.send("HELLO", { pingInterval: clientFacingPingInterval }); + session.send("HELLO", { pingInterval: clientFacingPingInterval, supportedAttributes }); return session; } @@ -177,8 +178,9 @@ class GatewayHandler { if (data.attributes) { if (!Array.isArray(data.attributes) || data.attributes.length > 8) return {error: wsCloseCodes.PAYLOAD_ERROR}; - for (let i = 0; i < data.attributes; i++) { - if (!supportedAttributes.includes(data[i])) return {error: wsCloseCodes.PAYLOAD_ERROR}; + for (let i = 0; i < data.attributes.length; i++) { + if (!supportedAttributes.includes(data.attributes[i])) + return {error: wsCloseCodes.UNSUPPORTED_ATTRIBUTE}; } session.attributes = data.attributes; } @@ -232,7 +234,20 @@ class GatewayHandler { // Check if the user is in that channel before broadcasting the message if (!session.channels.includes(data.channel._id)) return {error: wsCloseCodes.NOT_AUTHORIZED}; - this.eachInChannel({channelId: data.channel._id}, ({ session: remoteSession }) => { + this.eachInChannel({channelId: data.channel._id}, async ({ session: remoteSession }) => { + let id; + if (policies.allowSavingMessages) { + const message = await Message.create({ + author: session.user._id, + channel: data.channel._id, + content: messageContent, + createdAt: new Date().getTime() + }); + id = message._id; + } else { + id = new mongoose.Types.ObjectId(); + } + remoteSession.send("EVENT_CREATE_MESSAGE", { content: messageContent, channel: { @@ -242,18 +257,9 @@ class GatewayHandler { _id: session.user._id, username: session.user.username }, - _id: uuid.v4() + _id: id }); }); - - if (session.hasAttribute(attributes.SAVE_MESSAGES)) { - await Message.create({ - author: session.user._id, - channel: data.channel._id, - content: messageContent, - createdAt: new Date().getTime() - }); - } } } diff --git a/brainlet/config.js b/brainlet/config.js index d689a76..df0cb3d 100755 --- a/brainlet/config.js +++ b/brainlet/config.js @@ -18,6 +18,9 @@ module.exports = { allowAccountCreation: true, allowLogin: true, allowGatewayConnection: true, + // The policy below will make all messages sent over the gateway to be in plain text saved to the database. + // This is experimental and dangerous, and, as such, should generally not be used. + allowSavingMessages: true, perUserMaxGatewayConnections: 4 }, /* @@ -35,7 +38,6 @@ module.exports = { gatewayPingInterval: 15000, gatewayPingCheckInterval: 4500, clientFacingPingInterval: 14750, - unsafeStoreMessages: false, bcryptRounds: 10, roleMap: { "BANNED": 0, From 50cc87b399b55757e3d0af52513a4a779a14b70e Mon Sep 17 00:00:00 2001 From: hippoz Date: Sun, 3 Oct 2021 21:08:57 +0300 Subject: [PATCH 3/4] docs: update to reflect protocol changes --- resources/Docs/DOCS.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/Docs/DOCS.md b/resources/Docs/DOCS.md index 6b3483b..b256e59 100644 --- a/resources/Docs/DOCS.md +++ b/resources/Docs/DOCS.md @@ -26,11 +26,15 @@ Packets can also have JSON as a payload: Sent by the server to the client as soon as possible after they connect to the gateway. -This payload contains a `pingInterval` property. Every *pingInterval*, the client must send a packet simply containing `7@1`. This is the ACTION_PING payload. If the client does not send this payload at the right time, it is disconnected. +JSON data format: +| Field | Description | +| - | - | +| pingInterval | Every *pingInterval*, the client must send a packet simply containing `7@1`. This is the ACTION_PING payload. If the client does not send this payload at the right time, it is disconnected. | +| supportedAttributes | An array of attributes supported by the server. If a client requests an unsupported attribute, it is disconnected from the server. | Example: ```json -0@{"pingInterval":14750} +0@{"pingInterval":14750,"supportedAttributes":["PRESENCE_UPDATES"]} ``` ## 1:YOO @@ -194,7 +198,7 @@ Voice server signaling is done through a websocket gateway. This gateway is spec | 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) | -| _id | A UUIDv4 | +| _id | An ObjectId | ## Message channel object From 93b5531b5d819b0abc708e9fa0e708147080cb81 Mon Sep 17 00:00:00 2001 From: hippoz Date: Sun, 3 Oct 2021 21:11:51 +0300 Subject: [PATCH 4/4] fix: disable saving messages by default --- brainlet/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brainlet/config.js b/brainlet/config.js index df0cb3d..ed4754a 100755 --- a/brainlet/config.js +++ b/brainlet/config.js @@ -20,7 +20,7 @@ module.exports = { allowGatewayConnection: true, // The policy below will make all messages sent over the gateway to be in plain text saved to the database. // This is experimental and dangerous, and, as such, should generally not be used. - allowSavingMessages: true, + allowSavingMessages: false, perUserMaxGatewayConnections: 4 }, /*