Compare commits

..

2 commits

11 changed files with 175 additions and 175 deletions

View file

@ -1,5 +1,5 @@
# Brainlet # Brainlet
Brainlet is a simple chat app. Each category has a text channel associated with it, all messages sent in the text channel are temporary, while in the category itself all posts are permanent. Brainlet is a simple chat app. Each channel has a text channel associated with it, all messages sent in the text channel are temporary, while in the channel itself all posts are permanent.
# Resources # Resources

View file

@ -1,5 +1,5 @@
const User = require("../../models/User"); const User = require("../../models/User");
const Category = require("../../models/Category"); const Channel = require("../../models/Channel");
const Post = require("../../models/Post"); const Post = require("../../models/Post");
const config = require("../../config"); const config = require("../../config");
const { authenticateEndpoint } = require("./../../common/auth/authfunctions"); const { authenticateEndpoint } = require("./../../common/auth/authfunctions");
@ -17,7 +17,7 @@ const createLimiter = rateLimit({
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true}); mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
app.post("/category/create", [ app.post("/channel/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) => {
@ -28,7 +28,7 @@ app.post("/category/create", [
} }
const title = req.body.title; const title = req.body.title;
const category = await Category.create({ const channel = await Channel.create({
title: title, title: title,
creator: user._id, creator: user._id,
posts: [] posts: []
@ -37,13 +37,13 @@ app.post("/category/create", [
res.status(200).json({ res.status(200).json({
error: false, error: false,
message: "SUCCESS_CATEGORY_CREATED", message: "SUCCESS_CATEGORY_CREATED",
category: category.getPublicObject() channel: channel.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("channel").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) => {
@ -53,7 +53,7 @@ app.post("/post/create", [
return; return;
} }
const category = req.body.category; const channel = req.body.channel;
const title = req.body.title; const title = req.body.title;
const content = req.body.body; const content = req.body.body;
@ -61,10 +61,10 @@ app.post("/post/create", [
post.title = title; post.title = title;
post.body = content; post.body = content;
post.creator = user._id; post.creator = user._id;
post.category = category; post.channel = channel;
const r = await Category.updateOne({ const r = await Channel.updateOne({
_id: category _id: channel
}, { }, {
$push: { posts: post } $push: { posts: post }
}); });
@ -86,8 +86,8 @@ app.post("/post/create", [
}); });
}, undefined, config.roleMap.USER)); }, undefined, config.roleMap.USER));
app.get("/category/:category/info", [ app.get("/channel/:channel/info", [
param("category").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) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
@ -95,13 +95,13 @@ app.get("/category/:category/info", [
return; return;
} }
const categoryId = req.params.category; const channelId = req.params.channel;
const category = await Category.findById(categoryId).populate("posts.creator", User.getPulicFields()); const channel = await Channel.findById(channelId).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 (!channel) {
res.status(404).json({ res.status(404).json({
error: true, error: true,
message: "ERROR_CATEGORY_NOT_FOUND" message: "ERROR_CATEGORY_NOT_FOUND"
@ -112,7 +112,7 @@ app.get("/category/:category/info", [
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(), channel: channel.getPublicObject(),
userInfo: { userInfo: {
userListLimit: 50, userListLimit: 50,
users: users users: users
@ -120,19 +120,19 @@ app.get("/category/:category/info", [
}); });
}, undefined, config.roleMap.USER)); }, undefined, config.roleMap.USER));
app.get("/category/list", authenticateEndpoint(async (req, res) => { app.get("/channel/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().lean().sort({ _id: -1 }).limit(count).select("-posts -__v").populate("creator", User.getPulicFields()); const channels = await Channel.find().lean().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 channels
}); });
}, undefined, config.roleMap.USER)); }, undefined, config.roleMap.USER));

View file

@ -1,7 +1,7 @@
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 Channel = require("../../../models/Channel");
const RateLimiter = require("../../../common/util/ratelimiter"); const RateLimiter = require("../../../common/util/ratelimiter");
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
@ -23,15 +23,15 @@ class GatewayServer {
} }
} }
GatewayServer.prototype._sendSystemMessage = function(socket, message, category) { GatewayServer.prototype._sendSystemMessage = function(socket, message, channel) {
const messageObject = { const messageObject = {
author: { author: {
username: "__SYSTEM", username: "__SYSTEM",
_id: "5fc69864f15a7c5e504c9a1f" _id: "5fc69864f15a7c5e504c9a1f"
}, },
category: { channel: {
title: category.title, title: channel.title,
_id: category._id _id: channel._id
}, },
content: message, content: message,
_id: uuid.v4() _id: uuid.v4()
@ -53,7 +53,7 @@ GatewayServer.prototype._processCommand = async function(socket, message) {
switch (command) { switch (command) {
case "INVALID_COMMAND": { case "INVALID_COMMAND": {
this._sendSystemMessage(socket, "Invalid command.", message.category); this._sendSystemMessage(socket, "Invalid command.", message.channel);
break; break;
} }
case "admin/fr": { case "admin/fr": {
@ -61,33 +61,33 @@ GatewayServer.prototype._processCommand = async function(socket, message) {
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 { } else {
this._sendSystemMessage(socket, "how about no", message.category); this._sendSystemMessage(socket, "how about no", message.channel);
} }
} else { } else {
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category); this._sendSystemMessage(socket, "Invalid number of arguments.", message.channel);
} }
break; break;
} }
case "admin/fru": { case "admin/fru": {
if (args === 1) { if (args === 1) {
if (socket.user.permissionLevel >= config.roleMap.ADMIN) { if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
const user = await this._findSocketInRoom(message.category._id, fullCommand[1]); const user = await this._findSocketInRoom(message.channel._id, fullCommand[1]);
if (!user) { if (!user) {
this._sendSystemMessage(socket, "User not found.", message.category); this._sendSystemMessage(socket, "User not found.", message.channel);
break; break;
} }
this._gateway.in(user.user.sid).emit("refreshClient", { reason: "REFRESH" }); this._gateway.in(user.user.sid).emit("refreshClient", { reason: "REFRESH" });
} else { } else {
this._sendSystemMessage(socket, "how about no", message.category); this._sendSystemMessage(socket, "how about no", message.channel);
} }
} else { } else {
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category); this._sendSystemMessage(socket, "Invalid number of arguments.", message.channel);
} }
break; break;
} }
default: { default: {
this._sendSystemMessage(socket, "That command does not exist.", message.category); this._sendSystemMessage(socket, "That command does not exist.", message.channel);
break; break;
} }
} }
@ -170,8 +170,8 @@ GatewayServer.prototype.eventSetup = function() {
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 ({ channel, content, nickAuthor, destUser }) => {
if (!category || !content || !socket.joinedCategories || !socket.isConnected || !socket.user || !(typeof content === "string") || !(typeof category._id === "string")) return; if (!channel || !content || !socket.joinedChannels || !socket.isConnected || !socket.user || !(typeof content === "string") || !(typeof channel._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?
@ -179,9 +179,9 @@ GatewayServer.prototype.eventSetup = function() {
return; return;
} }
// TODO: When/if category permissions are added, check if the user has permissions for that category // TODO: When/if channel permissions are added, check if the user has permissions for that channel
const categoryTitle = socket.joinedCategories[category._id]; const channelTitle = socket.joinedChannels[channel._id];
if (!categoryTitle || !(typeof categoryTitle === "string")) return; if (!channelTitle || !(typeof channelTitle === "string")) return;
let messageObject = { let messageObject = {
author: { author: {
@ -189,9 +189,9 @@ GatewayServer.prototype.eventSetup = function() {
_id: socket.user._id, _id: socket.user._id,
color: socket.user.color color: socket.user.color
}, },
category: { channel: {
title: categoryTitle, title: channelTitle,
_id: category._id _id: channel._id
}, },
content: content, content: content,
_id: uuid.v4() _id: uuid.v4()
@ -214,32 +214,32 @@ GatewayServer.prototype.eventSetup = function() {
} }
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.channel._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(channel._id).emit("message", messageObject);
}); });
socket.on("subscribe", async (categories) => { socket.on("subscribe", async (channels) => {
if ( !socket.isConnected || !socket.user || !categories || !Array.isArray(categories) || categories === []) return; if ( !socket.isConnected || !socket.user || !channels || !Array.isArray(channels) || channels === []) return;
try { try {
for (const v of categories) { for (const v of channels) {
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 channel permissions are added, check if the user has permissions for that channel
const category = await Category.findById(v); const channel = await Channel.findById(v);
if (category && category.title && category._id) { if (channel && channel.title && channel._id) {
if (!socket.joinedCategories) socket.joinedCategories = {}; if (!socket.joinedChannels) socket.joinedChannels = {};
if (socket.joinedCategories[v]) continue; if (socket.joinedChannels[v]) continue;
socket.joinedCategories[v] = category.title; socket.joinedChannels[v] = channel.title;
await socket.join(v); await socket.join(v);
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} (${channel.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, channel.title);
this._gateway.in(v).emit("clientListUpdate", upd); this._gateway.in(v).emit("clientListUpdate", upd);
} }
} }
@ -255,10 +255,10 @@ GatewayServer.prototype.eventSetup = function() {
// 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 channelTitle = socket.joinedChannels[room] || "UNKNOWN";
await socket.leave(room); await socket.leave(room);
const upd = await this._generateClientListUpdateObject(room, categoryTitle); const upd = await this._generateClientListUpdateObject(room, channelTitle);
socket.in(room).emit("clientListUpdate", upd); socket.in(room).emit("clientListUpdate", upd);
}); });
}); });
@ -309,11 +309,11 @@ 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, channelTitle="UNKNOWN") {
const clientList = await this._getSocketsInRoom(room); const clientList = await this._getSocketsInRoom(room);
return { return {
category: { channel: {
title: categoryTitle, title: channelTitle,
_id: room _id: room
}, },
clientList clientList

View file

@ -3,7 +3,7 @@ const EventEmitter = require("events");
const uuid = require("uuid"); const uuid = require("uuid");
const User = require("../../../models/User"); const User = require("../../../models/User");
const Category = require("../../../models/Category"); const Channel = require("../../../models/Channel");
const { parseMessage, opcodeSeparator, getOpcodeByName } = require("./messageparser"); const { parseMessage, opcodeSeparator, getOpcodeByName } = require("./messageparser");
const { checkToken } = require("../../../common/auth/authfunctions"); const { checkToken } = require("../../../common/auth/authfunctions");
@ -57,7 +57,7 @@ class GatewayServer extends EventEmitter {
// The user is now successfully authenticated, send the YOO_ACK packet // The user is now successfully authenticated, send the YOO_ACK packet
// TODO: This is probably not efficient // TODO: This is probably not efficient
let channels = await Category.find().lean().sort({ _id: -1 }).limit(50).select("-posts -__v").populate("creator", User.getPulicFields(true)); let channels = await Channel.find().lean().sort({ _id: -1 }).limit(50).select("-posts -__v").populate("creator", User.getPulicFields(true));
if (!channels) channels = []; if (!channels) channels = [];
channels = channels.map(x => ({ ...x, _id: x._id.toString() })); channels = channels.map(x => ({ ...x, _id: x._id.toString() }));

View file

@ -77,24 +77,24 @@
</md-dialog-content> </md-dialog-content>
</md-dialog> </md-dialog>
<md-dialog id="create-category-dialog" :md-active.sync="dialog.show.createCategory"> <md-dialog id="create-channel-dialog" :md-active.sync="dialog.show.createChannel">
<md-dialog-title>Create category</md-dialog-title> <md-dialog-title>Create channel</md-dialog-title>
<md-dialog-content> <md-dialog-content>
<md-field> <md-field>
<label>Title</label> <label>Title</label>
<md-input v-model="dialog.text.createCategory.title"></md-input> <md-input v-model="dialog.text.createChannel.title"></md-input>
</md-field> </md-field>
<md-dialog-actions> <md-dialog-actions>
<md-button @click="dialog.show.createCategory = false">Close</md-button> <md-button @click="dialog.show.createChannel = false">Close</md-button>
<md-button class="md-primary" @click="createCategory()">Create</md-button> <md-button class="md-primary" @click="createChannel()">Create</md-button>
</md-dialog-actions> </md-dialog-actions>
</md-dialog-content> </md-dialog-content>
</md-dialog> </md-dialog>
<md-dialog id="create-post-dialog" :md-active.sync="dialog.show.createPost"> <md-dialog id="create-post-dialog" :md-active.sync="dialog.show.createPost">
<md-dialog-title>Create post for <strong>{{ selection.category.title }}</strong></md-dialog-title> <md-dialog-title>Create post for <strong>{{ selection.channel.title }}</strong></md-dialog-title>
<md-dialog-content> <md-dialog-content>
<md-field> <md-field>
@ -137,19 +137,19 @@
</md-menu> </md-menu>
</md-toolbar> </md-toolbar>
<md-toolbar v-show="selection.category.browsing" class="md-dense" md-elevation="5"> <md-toolbar v-show="selection.channel.browsing" class="md-dense" md-elevation="5">
<h3 v-if="selection.category.isCategory && !selection.category.isChatContext" class="md-title" style="flex: 1">Browsing category: {{ selection.category.title }}</h3> <h3 v-if="selection.channel.isChannel && !selection.channel.isChatContext" class="md-title" style="flex: 1">Browsing channel: {{ selection.channel.title }}</h3>
<h3 v-if="!selection.category.isCategory && !selection.category.isChatContext" class="md-title" style="flex: 1">Browsing {{ selection.category.title }}</h3> <h3 v-if="!selection.channel.isChannel && !selection.channel.isChatContext" class="md-title" style="flex: 1">Browsing {{ selection.channel.title }}</h3>
<h3 v-if="!selection.category.isCategory && selection.category.isChatContext" class="md-title" style="flex: 1"> <h3 v-if="!selection.channel.isChannel && selection.channel.isChatContext" class="md-title" style="flex: 1">
Browsing {{ selection.category.title }} with Browsing {{ selection.channel.title }} with
<a v-for="user in userLists[selection.category._id]" class="md-dense cursor" v-on:click="viewProfile(user.user._id)" v-bind:style="{ 'color': user.user.color }">{{ user.user.username }} </a> <a v-for="user in userLists[selection.channel._id]" class="md-dense cursor" v-on:click="viewProfile(user.user._id)" v-bind:style="{ 'color': user.user.color }">{{ user.user.username }} </a>
</h3> </h3>
<md-button @click="browseCategories()" v-if="selection.category.isCategory || selection.category.isChatContext"><md-icon>arrow_back</md-icon></md-button> <md-button @click="browseChannels()" v-if="selection.channel.isChannel || selection.channel.isChatContext"><md-icon>arrow_back</md-icon></md-button>
<md-button @click="refresh()" v-if="!selection.category.isChatContext"><md-icon>refresh</md-icon></md-button> <md-button @click="refresh()" v-if="!selection.channel.isChatContext"><md-icon>refresh</md-icon></md-button>
</md-toolbar> </md-toolbar>
<div id="posts-container" class="posts-container" v-if="selection.category.browsing"> <div id="posts-container" class="posts-container" v-if="selection.channel.browsing">
<md-card v-for="post in selection.posts" v-bind:key="post._id" v-if="!selection.category.isChatContext"> <md-card v-for="post in selection.posts" v-bind:key="post._id" v-if="!selection.channel.isChatContext">
<md-card-header> <md-card-header>
<div class="md-title" v-html="post.title"></div> <div class="md-title" v-html="post.title"></div>
<span>by <a class="md-dense cursor" v-on:click="viewProfile(post.creator._id)" v-bind:style="{ 'color': post.creator.color}">{{ post.creator.username }}</a></span> <span>by <a class="md-dense cursor" v-on:click="viewProfile(post.creator._id)" v-bind:style="{ 'color': post.creator.color}">{{ post.creator.username }}</a></span>
@ -161,7 +161,7 @@
<md-button v-for="button in cardButtons" v-bind:key="button.text" @click="button.click(post)"><md-icon>{{ button.text }}</md-icon></md-button> <md-button v-for="button in cardButtons" v-bind:key="button.text" @click="button.click(post)"><md-icon>{{ button.text }}</md-icon></md-button>
</md-card-actions> </md-card-actions>
</md-card> </md-card>
<div v-for="post,k in messages[selection.category._id]" v-if="selection.category.isChatContext" :key="post._id + post.author._id"> <div v-for="post,k in messages[selection.channel._id]" v-if="selection.channel.isChatContext" :key="post._id + post.author._id">
<md-card class="message-card"> <md-card class="message-card">
<md-card-header> <md-card-header>
<a v-if="!post.nickAuthor" class="md-dense cursor md-title" v-on:click="viewProfile(post.author._id)" v-bind:style="{ 'color': post.author.color}"><span>{{ post.author.username }}</span></a> <a v-if="!post.nickAuthor" class="md-dense cursor md-title" v-on:click="viewProfile(post.author._id)" v-bind:style="{ 'color': post.author.color}"><span>{{ post.author.username }}</span></a>
@ -174,26 +174,26 @@
</div> </div>
</div> </div>
<md-speed-dial class="md-fixed md-bottom-right" md-direction="top" md-elevation="5" style="z-index: 4000;" v-show="!selection.category.isChatContext"> <md-speed-dial class="md-fixed md-bottom-right" md-direction="top" md-elevation="5" style="z-index: 4000;" v-show="!selection.channel.isChatContext">
<md-speed-dial-target> <md-speed-dial-target>
<md-icon class="md-morph-initial">add</md-icon> <md-icon class="md-morph-initial">add</md-icon>
<md-icon class="md-morph-final">edit</md-icon> <md-icon class="md-morph-final">edit</md-icon>
</md-speed-dial-target> </md-speed-dial-target>
<md-speed-dial-content> <md-speed-dial-content>
<md-button v-show="selection.category.isCategory" class="md-icon-button" @click="showCreatePostDialog()"> <md-button v-show="selection.channel.isChannel" class="md-icon-button" @click="showCreatePostDialog()">
<md-icon>add</md-icon> <md-icon>add</md-icon>
<md-tooltip md-direction="left">Create a new post</md-tooltip> <md-tooltip md-direction="left">Create a new post</md-tooltip>
</md-button> </md-button>
<md-button class="md-icon-button" @click="dialog.show.createCategory = true"> <md-button class="md-icon-button" @click="dialog.show.createChannel = true">
<md-icon>category</md-icon> <md-icon>category</md-icon>
<md-tooltip md-direction="left">Create a new category</md-tooltip> <md-tooltip md-direction="left">Create a new channel</md-tooltip>
</md-button> </md-button>
</md-speed-dial-content> </md-speed-dial-content>
</md-speed-dial> </md-speed-dial>
<md-field md-inline class="chat-bar" v-show="selection.category.isChatContext"> <md-field md-inline class="chat-bar" v-show="selection.channel.isChatContext">
<label>Write something interesting, go on!</label> <label>Write something interesting, go on!</label>
<md-input v-model="message.typed" v-on:keyup.enter="sendCurrentMessage()"></md-input> <md-input v-model="message.typed" v-on:keyup.enter="sendCurrentMessage()"></md-input>
</md-field> </md-field>

View file

@ -10,8 +10,8 @@ const getCreatePostError = (json) => {
case 'body': { case 'body': {
return 'Invalid content. Must be between 3 and 1000 characters'; return 'Invalid content. Must be between 3 and 1000 characters';
} }
case 'category': { case 'channel': {
return 'Invalid category. Something went wrong.'; return 'Invalid channel. Something went wrong.';
} }
default: { default: {
return 'Invalid value sent to server. Something went wrong.'; return 'Invalid value sent to server. Something went wrong.';
@ -20,7 +20,7 @@ const getCreatePostError = (json) => {
} }
case 'ERROR_CATEGORY_NOT_FOUND': { case 'ERROR_CATEGORY_NOT_FOUND': {
return 'The category you tried to post to no longer exists.'; return 'The channel you tried to post to no longer exists.';
} }
case 'ERROR_ACCESS_DENIED': { case 'ERROR_ACCESS_DENIED': {
@ -33,7 +33,7 @@ const getCreatePostError = (json) => {
} }
} }
const getCreateCategoryError = (json) => { const getCreateChannelError = (json) => {
switch (json.message) { switch (json.message) {
case 'ERROR_REQUEST_INVALID_DATA': { case 'ERROR_REQUEST_INVALID_DATA': {
switch (json.errors[0].param) { switch (json.errors[0].param) {
@ -109,22 +109,22 @@ GatewayConnection.prototype.connect = function(token) {
}); });
}; };
GatewayConnection.prototype.sendMessage = function(categoryId, content) { GatewayConnection.prototype.sendMessage = function(channelId, content) {
if (!this.isConnected) return 1; if (!this.isConnected) return 1;
if (content.length >= 2000) return 1; if (content.length >= 2000) return 1;
this.socket.emit('message', { this.socket.emit('message', {
category: { channel: {
_id: categoryId _id: channelId
}, },
content content
}); });
}; };
GatewayConnection.prototype.subscribeToCategoryChat = function(categoryId) { GatewayConnection.prototype.subscribeToChannelChat = function(channelId) {
if (!this.isConnected) return; if (!this.isConnected) return;
const request = [categoryId]; const request = [channelId];
console.log('[*] [gateway] Subscribing to channel(s)', request); console.log('[*] [gateway] Subscribing to channel(s)', request);
@ -143,11 +143,11 @@ const app = new Vue({
showApp: false, showApp: false,
menuVisible: false, menuVisible: false,
selection: { selection: {
category: { channel: {
title: '', title: '',
browsing: false, browsing: false,
_id: undefined, _id: undefined,
isCategory: false, isChannel: false,
isChatContext: false isChatContext: false
}, },
posts: [] posts: []
@ -156,7 +156,7 @@ const app = new Vue({
dialog: { dialog: {
show: { show: {
createPost: false, createPost: false,
createCategory: false, createChannel: false,
debug: false debug: false
}, },
text: { text: {
@ -164,7 +164,7 @@ const app = new Vue({
title: '', title: '',
body: '' body: ''
}, },
createCategory: { createChannel: {
title: '' title: ''
} }
} }
@ -201,7 +201,7 @@ const app = new Vue({
this.loggedInUser = json.user; this.loggedInUser = json.user;
this.showApp = true; this.showApp = true;
this.performGatewayConnection(); this.performGatewayConnection();
this.browseCategories(); this.browseChannels();
Notification.requestPermission(); Notification.requestPermission();
} else { } else {
this.showApp = false; this.showApp = false;
@ -259,9 +259,9 @@ const app = new Vue({
}); });
}); });
}, },
shouldMergeMessage: function(messageObject, categoryMessageList) { shouldMergeMessage: function(messageObject, channelMessageList) {
const lastMessageIndex = categoryMessageList.length-1; const lastMessageIndex = channelMessageList.length-1;
const lastMessage = categoryMessageList[lastMessageIndex]; const lastMessage = channelMessageList[lastMessageIndex];
if (!lastMessage) return; if (!lastMessage) return;
if (lastMessage.author._id === messageObject.author._id) { if (lastMessage.author._id === messageObject.author._id) {
@ -277,32 +277,32 @@ const app = new Vue({
return false; return false;
}, },
processMessage: async function(messageObject) { processMessage: async function(messageObject) {
if (!this.messages[messageObject.category._id]) this.$set(this.messages, messageObject.category._id, []); if (!this.messages[messageObject.channel._id]) this.$set(this.messages, messageObject.channel._id, []);
const categoryMessageList = this.messages[messageObject.category._id]; const channelMessageList = this.messages[messageObject.channel._id];
const lastMessageIndex = categoryMessageList.length-1; const lastMessageIndex = channelMessageList.length-1;
if (this.shouldMergeMessage(messageObject, categoryMessageList)) { if (this.shouldMergeMessage(messageObject, channelMessageList)) {
categoryMessageList[lastMessageIndex].content += `\n${messageObject.content}`; channelMessageList[lastMessageIndex].content += `\n${messageObject.content}`;
} else { } else {
this.messages[messageObject.category._id].push(messageObject); this.messages[messageObject.channel._id].push(messageObject);
} }
if (messageObject.category._id === this.selection.category._id) { if (messageObject.channel._id === this.selection.channel._id) {
this.$nextTick(() => { this.$nextTick(() => {
// TODO: When the user presses back, actually undo this scroll cause its annoying to scroll back up in the category list // TODO: When the user presses back, actually undo this scroll cause its annoying to scroll back up in the channel list
const container = this.$el.querySelector('#posts-container'); const container = this.$el.querySelector('#posts-container');
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
}); });
} }
if (messageObject.author.username !== this.loggedInUser.username && messageObject.category._id !== this.selection.category._id) { if (messageObject.author.username !== this.loggedInUser.username && messageObject.channel._id !== this.selection.channel._id) {
this.okNotification(`${messageObject.category.title}/${messageObject.author.username}: ${messageObject.content}`); this.okNotification(`${messageObject.channel.title}/${messageObject.author.username}: ${messageObject.content}`);
} }
if (messageObject.author.username !== this.loggedInUser.username) { if (messageObject.author.username !== this.loggedInUser.username) {
if (Notification.permission === 'granted') { if (Notification.permission === 'granted') {
try { try {
new Notification(`${messageObject.category.title}/${messageObject.author.username}`, { new Notification(`${messageObject.channel.title}/${messageObject.author.username}`, {
body: messageObject.content body: messageObject.content
}); });
} catch(e) { } catch(e) {
@ -313,20 +313,20 @@ const app = new Vue({
} }
}, },
processUserListUpdate: async function(e) { processUserListUpdate: async function(e) {
const { category, clientList } = e; const { channel, clientList } = e;
if (!this.userLists[category._id]) this.$set(this.userLists, category._id, []); if (!this.userLists[channel._id]) this.$set(this.userLists, channel._id, []);
this.userLists[category._id] = clientList; this.userLists[channel._id] = clientList;
}, },
openChatForCategory: async function(categoryId) { openChatForChannel: async function(channelId) {
this.gateway.subscribeToCategoryChat(categoryId); this.gateway.subscribeToChannelChat(channelId);
this.selection.category.isChatContext = true; this.selection.channel.isChatContext = true;
this.selection.category.browsing = true; this.selection.channel.browsing = true;
this.selection.category.title = 'Chat'; this.selection.channel.title = 'Chat';
this.selection.category._id = categoryId; this.selection.channel._id = channelId;
}, },
sendCurrentMessage: async function() { sendCurrentMessage: async function() {
const status = await this.gateway.sendMessage(this.selection.category._id, this.message.typed); const status = await this.gateway.sendMessage(this.selection.channel._id, this.message.typed);
if (status === 1) { if (status === 1) {
this.okNotification('Failed to send message!'); this.okNotification('Failed to send message!');
return; return;
@ -343,9 +343,9 @@ const app = new Vue({
console.log('[DEBUG DUMP] [loggedInUser] (this contains sensitive information about the current logged in user, do not leak it to other people lol)', this.loggedInUser); console.log('[DEBUG DUMP] [loggedInUser] (this contains sensitive information about the current logged in user, do not leak it to other people lol)', this.loggedInUser);
}, },
// Category and post browsing // Channel and post browsing
browseCategories: async function() { browseChannels: async function() {
const res = await fetch(`${window.location.origin}/api/v1/content/category/list?count=50`, { const res = await fetch(`${window.location.origin}/api/v1/content/channel/list?count=50`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -356,36 +356,36 @@ const app = new Vue({
if (res.ok) { if (res.ok) {
const json = await res.json(); const json = await res.json();
this.selection.category.title = 'categories'; this.selection.channel.title = 'channels';
this.selection.category.browsing = true; this.selection.channel.browsing = true;
this.selection.category.isCategory = false; this.selection.channel.isChannel = false;
this.selection.category.isChatContext = false; this.selection.channel.isChatContext = false;
this.selection.category._id = '__CATEGORY_LIST'; this.selection.channel._id = '__CATEGORY_LIST';
this.selection.posts = []; this.selection.posts = [];
this.cardButtons = []; this.cardButtons = [];
this.button('chat', (post) => { this.button('chat', (post) => {
if (post._id) { if (post._id) {
this.openChatForCategory(post._id); this.openChatForChannel(post._id);
} }
}); });
this.button('topic', (post) => { this.button('topic', (post) => {
this.browse(post); this.browse(post);
}); });
for (let i = 0; i < json.categories.length; i++) { for (let i = 0; i < json.channels.length; i++) {
const v = json.categories[i]; const v = json.channels[i];
this.selection.posts.push({ title: v.title, body: '', _id: v._id, creator: v.creator }); this.selection.posts.push({ title: v.title, body: '', _id: v._id, creator: v.creator });
} }
} else { } else {
this.okNotification('Failed to fetch category list'); this.okNotification('Failed to fetch channel list');
} }
}, },
browse: async function(category) { browse: async function(channel) {
const { _id, title } = category; const { _id, title } = channel;
const res = await fetch(`${window.location.origin}/api/v1/content/category/${_id}/info`, { const res = await fetch(`${window.location.origin}/api/v1/content/channel/${_id}/info`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -396,39 +396,39 @@ const app = new Vue({
if (res.ok) { if (res.ok) {
const json = await res.json(); const json = await res.json();
this.selection.category.title = title; this.selection.channel.title = title;
this.selection.category._id = _id; this.selection.channel._id = _id;
this.selection.category.browsing = true; this.selection.channel.browsing = true;
this.selection.category.isCategory = true; this.selection.channel.isChannel = true;
this.selection.category.isChatContext = false; this.selection.channel.isChatContext = false;
this.selection.posts = []; this.selection.posts = [];
this.cardButtons = []; this.cardButtons = [];
for (let i = 0; i < json.category.posts.length; i++) { for (let i = 0; i < json.channel.posts.length; i++) {
const v = json.category.posts[i]; const v = json.channel.posts[i];
this.selection.posts.push({ title: v.title, body: v.body, _id: v._id, creator: v.creator }); this.selection.posts.push({ title: v.title, body: v.body, _id: v._id, creator: v.creator });
} }
} else { } else {
this.okNotification('Failed to fetch category'); this.okNotification('Failed to fetch channel');
} }
}, },
refresh: function() { refresh: function() {
if (this.selection.category.title === 'categories' && this.selection.category.isCategory === false) { if (this.selection.channel.title === 'channels' && this.selection.channel.isChannel === false) {
this.browseCategories(); this.browseChannels();
} else { } else {
this.browse(this.selection.category); this.browse(this.selection.channel);
} }
}, },
button: function(text, click) { button: function(text, click) {
this.cardButtons.push({ text, click }); this.cardButtons.push({ text, click });
}, },
stopBrowsing: function() { stopBrowsing: function() {
this.selection.category = { this.selection.channel = {
title: '', title: '',
browsing: false, browsing: false,
_id: undefined, _id: undefined,
isCategory: false, isChannel: false,
isChatContext: false isChatContext: false
}; };
this.selection.posts = []; this.selection.posts = [];
@ -457,20 +457,20 @@ const app = new Vue({
// Content creation // Content creation
showCreatePostDialog: function() { showCreatePostDialog: function() {
if (!this.selection.category.isCategory) { if (!this.selection.channel.isChannel) {
this.okNotification('You are not in a category'); this.okNotification('You are not in a channel');
return; return;
} }
this.dialog.show.createPost = true; this.dialog.show.createPost = true;
}, },
createPost: async function() { createPost: async function() {
if (!this.selection.category.isCategory) { if (!this.selection.channel.isChannel) {
this.okNotification('You are not in a category'); this.okNotification('You are not in a channel');
return; return;
} }
const category = this.selection.category; const channel = this.selection.channel;
const input = this.dialog.text.createPost; const input = this.dialog.text.createPost;
const res = await fetch(`${window.location.origin}/api/v1/content/post/create`, { const res = await fetch(`${window.location.origin}/api/v1/content/post/create`, {
@ -481,7 +481,7 @@ const app = new Vue({
}, },
credentials: 'include', credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
category: category._id, channel: channel._id,
title: input.title, title: input.title,
body: input.body body: input.body
}) })
@ -490,7 +490,7 @@ const app = new Vue({
if (res.ok) { if (res.ok) {
this.okNotification('Successfully created post'); this.okNotification('Successfully created post');
this.dialog.show.createPost = false; this.dialog.show.createPost = false;
this.browse(this.selection.category); this.browse(this.selection.channel);
return; return;
} else { } else {
if (res.status === 401 || res.status === 403) { if (res.status === 401 || res.status === 403) {
@ -507,10 +507,10 @@ const app = new Vue({
return; return;
} }
}, },
createCategory: async function() { createChannel: async function() {
const input = this.dialog.text.createCategory; const input = this.dialog.text.createChannel;
const res = await fetch(`${window.location.origin}/api/v1/content/category/create`, { const res = await fetch(`${window.location.origin}/api/v1/content/channel/create`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -523,9 +523,9 @@ const app = new Vue({
}); });
if (res.ok) { if (res.ok) {
this.okNotification('Successfully created category'); this.okNotification('Successfully created channel');
this.dialog.show.createCategory = false; this.dialog.show.createChannel = false;
this.browseCategories(); this.browseChannels();
return; return;
} else { } else {
if (res.status === 401 || res.status === 403) { if (res.status === 401 || res.status === 403) {
@ -538,7 +538,7 @@ const app = new Vue({
} }
const json = await res.json(); const json = await res.json();
this.okNotification(getCreateCategoryError(json)); this.okNotification(getCreateChannelError(json));
return; return;
} }
}, },

View file

@ -2,13 +2,13 @@ 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 channelSchema = 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() { channelSchema.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,
@ -17,6 +17,6 @@ categorySchema.method("getPublicObject", function() {
}; };
}); });
const Category = mongoose.model("Category", categorySchema); const Channel = mongoose.model("Channel", channelSchema);
module.exports = Category; module.exports = Channel;

View file

@ -4,7 +4,7 @@ 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"} channelId: {type: mongoose.Schema.Types.ObjectId, ref: "Channel"}
}); });
module.exports = Post; module.exports = Post;

View file

@ -50,7 +50,7 @@ const main = async () => {
discord.login(secret.discord.token); discord.login(secret.discord.token);
const category = client.gateway.subscribeToCategoryChat(LISTEN_ON); const channel = client.gateway.subscribeToChannelChat(LISTEN_ON);
discord.on('message', (e) => { discord.on('message', (e) => {
if (e.webhookID) return; if (e.webhookID) return;

View file

@ -59,13 +59,13 @@ GatewayConnection.prototype.connect = function(token) {
this.socket.on('clientListUpdate', (e) => this.emit('clientListUpdate', e)); this.socket.on('clientListUpdate', (e) => this.emit('clientListUpdate', e));
}; };
GatewayConnection.prototype.sendMessage = function(categoryId, content, { nickAuthor, destUser }) { GatewayConnection.prototype.sendMessage = function(channelId, content, { nickAuthor, destUser }) {
if (!this.isConnected) return 1; if (!this.isConnected) return 1;
if (content.length >= 2000) return 1; if (content.length >= 2000) return 1;
this.socket.emit('message', { this.socket.emit('message', {
category: { channel: {
_id: categoryId _id: channelId
}, },
nickAuthor, nickAuthor,
destUser, destUser,
@ -73,16 +73,16 @@ GatewayConnection.prototype.sendMessage = function(categoryId, content, { nickAu
}); });
}; };
GatewayConnection.prototype.subscribeToCategoryChat = function(categoryId) { GatewayConnection.prototype.subscribeToChannelChat = function(channelId) {
if (!this.isConnected) return; if (!this.isConnected) return;
const request = [categoryId]; const request = [channelId];
console.log('[*] [gateway] Subscribing to channel(s)', request); console.log('[*] [gateway] Subscribing to channel(s)', request);
this.socket.emit('subscribe', request); this.socket.emit('subscribe', request);
return categoryId; return channelId;
}; };
module.exports = GatewayConnection; module.exports = GatewayConnection;

View file

@ -23,7 +23,7 @@ Packets can also have JSON as a payload:
## Instructions ## Instructions
The terms "channel" and "category" are used interchangeably. The terms "channel" and "channel" are used interchangeably.
## 0:HELLO ## 0:HELLO
*Part of handshake, Server to client* *Part of handshake, Server to client*