diff --git a/brainlet/api/v1/content.js b/brainlet/api/v1/content.js index 1a01e4a..03bae5b 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,43 @@ 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) => { + 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() }); + 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 +137,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 +171,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..3c3ba59 100644 --- a/brainlet/api/v2/gateway/index.js +++ b/brainlet/api/v2/gateway/index.js @@ -1,10 +1,12 @@ 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"); 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"); @@ -17,10 +19,11 @@ 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" + PRESENCE_UPDATES: "PRESENCE_UPDATES", }; const supportedAttributes = [attributes.PRESENCE_UPDATES]; @@ -30,7 +33,7 @@ class GatewaySession { this.authenticated = false; this.user = null; this.token = null; - this.sessionId = uuid.v4(); + this.sessionId = v4(); this.attributes = []; // Specific to websocket sessions @@ -108,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; } @@ -175,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; } @@ -230,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: { @@ -240,7 +257,7 @@ class GatewayHandler { _id: session.user._id, username: session.user.username }, - _id: uuid.v4() + _id: id }); }); } diff --git a/brainlet/config.js b/brainlet/config.js index 656dc88..ed4754a 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,10 @@ module.exports = { allowAccountCreation: true, allowLogin: true, allowGatewayConnection: true, - perUserMaxGatewayConnections: 4, + // 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: false, + perUserMaxGatewayConnections: 4 }, /* --- Adding a special code requirement for account creation 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 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