feat: add message storage and fetching api #21

Merged
hippoz merged 4 commits from message-history into master 2021-10-03 21:20:29 +03:00
5 changed files with 92 additions and 21 deletions

View file

@ -1,6 +1,7 @@
const User = require("../../models/User"); const User = require("../../models/User");
const Channel = require("../../models/Channel"); const Channel = require("../../models/Channel");
const Post = require("../../models/Post"); const Post = require("../../models/Post");
const Message = require("../../models/Message");
const config = require("../../config"); const config = require("../../config");
const { authenticateEndpoint } = require("./../../common/auth/authfunctions"); const { authenticateEndpoint } = require("./../../common/auth/authfunctions");
@ -38,7 +39,7 @@ app.post("/channel/create", [
res.status(200).json({ res.status(200).json({
error: false, error: false,
message: "SUCCESS_CATEGORY_CREATED", message: "SUCCESS_CHANNEL_CREATED",
channel: channel.getPublicObject() channel: channel.getPublicObject()
}); });
}, undefined, config.roleMap.USER)); }, undefined, config.roleMap.USER));
@ -76,7 +77,7 @@ app.post("/post/create", [
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_CHANNEL_NOT_FOUND"
}); });
return; return;
} }
@ -90,6 +91,43 @@ app.post("/post/create", [
}); });
}, undefined, config.roleMap.USER)); }, 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", [ app.get("/channel/:channel/info", [
param("channel").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }) param("channel").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
], authenticateEndpoint(async (req, res) => { ], authenticateEndpoint(async (req, res) => {
@ -99,23 +137,21 @@ app.get("/channel/:channel/info", [
return; return;
} }
const channelId = req.params.channel; const channel = await Channel.findById(req.params.channel).populate("posts.creator", User.getPulicFields());
const channel = await Channel.findById(channelId).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 (!channel) { if (!channel) {
res.status(404).json({ res.status(404).json({
error: true, error: true,
message: "ERROR_CATEGORY_NOT_FOUND" message: "ERROR_CHANNEL_NOT_FOUND"
}); });
return; return;
} }
res.status(200).json({ res.status(200).json({
error: false, error: false,
message: "SUCCESS_CATEGORY_DATA_FETCHED", message: "SUCCESS_CHANNEL_DATA_FETCHED",
channel: channel.getPublicObject(), channel: channel.getPublicObject(),
userInfo: { userInfo: {
userListLimit: 50, userListLimit: 50,
@ -135,7 +171,7 @@ app.get("/channel/list", authenticateEndpoint(async (req, res) => {
res.status(200).json({ res.status(200).json({
error: false, error: false,
message: "SUCCESS_CATEGORY_LIST_FETCHED", message: "SUCCESS_CHANNEL_LIST_FETCHED",
channels channels
}); });
}, undefined, config.roleMap.USER)); }, undefined, config.roleMap.USER));

View file

@ -1,10 +1,12 @@
const websockets = require("ws"); const websockets = require("ws");
const uuid = require("uuid"); const { v4 } = require("uuid");
const mongoose = require("mongoose");
const { policies, gatewayPingInterval, gatewayPingCheckInterval, clientFacingPingInterval } = require("../../../config"); const { policies, gatewayPingInterval, gatewayPingCheckInterval, clientFacingPingInterval } = require("../../../config");
const { experiments } = require("../../../experiments"); const { experiments } = require("../../../experiments");
const User = require("../../../models/User"); const User = require("../../../models/User");
const Channel = require("../../../models/Channel"); const Channel = require("../../../models/Channel");
const Message = require("../../../models/Message");
const { parseMessage, packet } = require("./messageparser"); const { parseMessage, packet } = require("./messageparser");
const { checkToken } = require("../../../common/auth/authfunctions"); const { checkToken } = require("../../../common/auth/authfunctions");
@ -17,10 +19,11 @@ const wsCloseCodes = {
NOT_AUTHORIZED: [4006, "Not authorized"], NOT_AUTHORIZED: [4006, "Not authorized"],
FLOODING: [4007, "Flooding"], FLOODING: [4007, "Flooding"],
NO_PING: [4008, "No ping"], NO_PING: [4008, "No ping"],
UNSUPPORTED_ATTRIBUTE: [4009, "Unsupported attribute."],
}; };
const attributes = { const attributes = {
PRESENCE_UPDATES: "PRESENCE_UPDATES" PRESENCE_UPDATES: "PRESENCE_UPDATES",
}; };
const supportedAttributes = [attributes.PRESENCE_UPDATES]; const supportedAttributes = [attributes.PRESENCE_UPDATES];
@ -30,7 +33,7 @@ class GatewaySession {
this.authenticated = false; this.authenticated = false;
this.user = null; this.user = null;
this.token = null; this.token = null;
this.sessionId = uuid.v4(); this.sessionId = v4();
this.attributes = []; this.attributes = [];
// Specific to websocket sessions // Specific to websocket sessions
@ -108,7 +111,7 @@ class GatewayHandler {
const session = new GatewaySession(); const session = new GatewaySession();
session.setWebsocketClient(ws); session.setWebsocketClient(ws);
session.send("HELLO", { pingInterval: clientFacingPingInterval }); session.send("HELLO", { pingInterval: clientFacingPingInterval, supportedAttributes });
return session; return session;
} }
@ -175,8 +178,9 @@ class GatewayHandler {
if (data.attributes) { if (data.attributes) {
if (!Array.isArray(data.attributes) || data.attributes.length > 8) return {error: wsCloseCodes.PAYLOAD_ERROR}; if (!Array.isArray(data.attributes) || data.attributes.length > 8) return {error: wsCloseCodes.PAYLOAD_ERROR};
for (let i = 0; i < data.attributes; i++) { for (let i = 0; i < data.attributes.length; i++) {
if (!supportedAttributes.includes(data[i])) return {error: wsCloseCodes.PAYLOAD_ERROR}; if (!supportedAttributes.includes(data.attributes[i]))
return {error: wsCloseCodes.UNSUPPORTED_ATTRIBUTE};
} }
session.attributes = data.attributes; session.attributes = data.attributes;
} }
@ -230,7 +234,20 @@ class GatewayHandler {
// Check if the user is in that channel before broadcasting the message // Check if the user is in that channel before broadcasting the message
if (!session.channels.includes(data.channel._id)) return {error: wsCloseCodes.NOT_AUTHORIZED}; 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", { remoteSession.send("EVENT_CREATE_MESSAGE", {
content: messageContent, content: messageContent,
channel: { channel: {
@ -240,7 +257,7 @@ class GatewayHandler {
_id: session.user._id, _id: session.user._id,
username: session.user.username username: session.user.username
}, },
_id: uuid.v4() _id: id
}); });
}); });
} }

View file

@ -10,7 +10,6 @@ module.exports = {
// "https://example.com" // "https://example.com"
"http://localhost:3005", // Allow the server itself (provided it's listening on 3005) "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: { policies: {
// Currently, policies apply to all users - no matter the role. // Currently, policies apply to all users - no matter the role.
@ -19,7 +18,10 @@ module.exports = {
allowAccountCreation: true, allowAccountCreation: true,
allowLogin: true, allowLogin: true,
allowGatewayConnection: 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 --- Adding a special code requirement for account creation

View file

@ -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;

View file

@ -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. 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: Example:
```json ```json
0@{"pingInterval":14750} 0@{"pingInterval":14750,"supportedAttributes":["PRESENCE_UPDATES"]}
``` ```
## 1:YOO ## 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) | | content | The text content of the message (max 2000 characters, min 1 character, trimmed) |
| channel | A [message channel object](#message-channel-object) | | channel | A [message channel object](#message-channel-object) |
| author | A [message author object](#message-author-object) | | author | A [message author object](#message-author-object) |
| _id | A UUIDv4 | | _id | An ObjectId |
## Message channel object ## Message channel object