diff --git a/.env.example b/.env.example index 3098642..749ff22 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ VUE_APP_MATOMO_URL=https://analytics.flowinity.com VUE_APP_MATOMO_TRACKER=flow VUE_APP_MATOMO_DOMAINS=*.troplo.com VUE_APP_MATOMO_ENABLED=true -VUE_APP_SENTRY_ENABLED=true \ No newline at end of file +VUE_APP_SENTRY_ENABLED=true +VUE_APP_BASE_URL= \ No newline at end of file diff --git a/backend/lib/authorize_socket.js b/backend/lib/authorize_socket.js index 68284b9..34fb4a7 100644 --- a/backend/lib/authorize_socket.js +++ b/backend/lib/authorize_socket.js @@ -34,12 +34,36 @@ module.exports = async function (socket, next) { next() } } else { - // + socket.user = { + id: null, + username: "Not Authenticated" + } + socket.compassUser = { + id: null, + username: "BC-NOAUTH" + } + next() } } else { - // + socket.user = { + id: null, + username: "Not Authenticated" + } + socket.compassUser = { + id: null, + username: "BC-NOAUTH" + } + next() } } catch (error) { - // + socket.user = { + id: null, + username: "Not Authenticated" + } + socket.compassUser = { + id: null, + username: "BC-NOAUTH" + } + next() } } diff --git a/backend/lib/socket.js b/backend/lib/socket.js index dcfc010..561fbf4 100644 --- a/backend/lib/socket.js +++ b/backend/lib/socket.js @@ -15,88 +15,115 @@ module.exports = { id: socket.user.id } }) - socket.join(user.id) - socket.emit("siteState", { - release: process.env.RELEASE, - notification: process.env.NOTIFICATION, - notificationType: process.env.NOTIFICATION_TYPE, - latestVersion: require("../../package.json").version - }) - const friends = await Friend.findAll({ - where: { - userId: user.id, - status: "accepted" - } - }) - await user.update({ - status: - user.storedStatus === "invisible" ? "offline" : user.storedStatus - }) - friends.forEach((friend) => { - io.to(friend.friendId).emit("userStatus", { - userId: user.id, + if (user && socket.user.id) { + console.log(socket.user.id) + socket.join(user.id) + socket.emit("siteState", { + release: process.env.RELEASE, + notification: process.env.NOTIFICATION, + notificationType: process.env.NOTIFICATION_TYPE, + latestVersion: require("../../package.json").version + }) + const friends = await Friend.findAll({ + where: { + userId: user.id, + status: "accepted" + } + }) + await user.update({ status: user.storedStatus === "invisible" ? "offline" : user.storedStatus }) - }) - socket.on("ping", () => { - socket.emit("pong") - }) - socket.on("idle", async () => { - const user = await User.findOne({ - where: { - id: socket.user.id - } - }) - if (user.storedStatus === "online") { - friends.forEach((friend) => { - io.to(friend.friendId).emit("userStatus", { - userId: user.id, - status: "away" - }) - }) - io.to(user.id).emit("userStatus", { - userId: user.id, - status: "away" - }) - await user.update({ - status: "away" - }) - } - }) - socket.on("online", async () => { - const user = await User.findOne({ - where: { - id: socket.user.id - } - }) - if (user.storedStatus === "online") { - friends.forEach((friend) => { - io.to(friend.friendId).emit("userStatus", { - userId: user.id, - status: "online" - }) - }) - io.to(user.id).emit("userStatus", { - userId: user.id, - status: "online" - }) - await user.update({ - status: "online" - }) - } - }) - socket.on("disconnect", async function () { friends.forEach((friend) => { io.to(friend.friendId).emit("userStatus", { userId: user.id, + status: + user.storedStatus === "invisible" ? "offline" : user.storedStatus + }) + }) + socket.on("ping", () => { + socket.emit("pong") + }) + socket.on("bcBots/deleteMessage", (e) => { + if (socket.user.bot) { + socket.to(e.userId).emit("deleteMessage", e) + } else { + socket.emit("bcBots/deleteMessage", { + error: "You cannot perform this action." + }) + } + }) + socket.on("idle", async () => { + const user = await User.findOne({ + where: { + id: socket.user.id + } + }) + if (user.storedStatus === "online") { + friends.forEach((friend) => { + io.to(friend.friendId).emit("userStatus", { + userId: user.id, + status: "away" + }) + }) + io.to(user.id).emit("userStatus", { + userId: user.id, + status: "away" + }) + await user.update({ + status: "away" + }) + } + }) + socket.on("online", async () => { + const user = await User.findOne({ + where: { + id: socket.user.id + } + }) + if (user.storedStatus === "online") { + friends.forEach((friend) => { + io.to(friend.friendId).emit("userStatus", { + userId: user.id, + status: "online" + }) + }) + io.to(user.id).emit("userStatus", { + userId: user.id, + status: "online" + }) + await user.update({ + status: "online" + }) + } + }) + socket.on("disconnect", async function () { + friends.forEach((friend) => { + io.to(friend.friendId).emit("userStatus", { + userId: user.id, + status: "offline" + }) + }) + await user.update({ status: "offline" }) }) - await user.update({ - status: "offline" + } else { + socket.join(-1) + socket.emit("siteState", { + release: process.env.RELEASE, + notification: process.env.NOTIFICATION, + notificationType: process.env.NOTIFICATION_TYPE, + latestVersion: require("../../package.json").version }) - }) + socket.emit("unauthorized", { + message: "Please reauth." + }) + console.log("Unauthenticated user") + socket.on("reAuth", async () => { + socket.disconnect() + }) + } }) console.log("WS OK") app.set("io", io) diff --git a/backend/migrations/20220706061900-nicknames.js b/backend/migrations/20220706061900-nicknames.js new file mode 100644 index 0000000..126ba1c --- /dev/null +++ b/backend/migrations/20220706061900-nicknames.js @@ -0,0 +1,43 @@ +"use strict" + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("Nicknames", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + userId: { + allowNull: false, + type: Sequelize.BIGINT + }, + nickname: { + allowNull: false, + type: Sequelize.STRING + }, + friendId: { + allowNull: false, + type: Sequelize.BIGINT + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }) + }, + + async down(queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + } +} diff --git a/backend/migrations/20220712133850-bot.js b/backend/migrations/20220712133850-bot.js new file mode 100644 index 0000000..bd3f124 --- /dev/null +++ b/backend/migrations/20220712133850-bot.js @@ -0,0 +1,20 @@ +"use strict" + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn("Users", "bot", { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false + }) + }, + + async down(queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + } +} diff --git a/backend/models/nicknames.js b/backend/models/nicknames.js new file mode 100644 index 0000000..49dd326 --- /dev/null +++ b/backend/models/nicknames.js @@ -0,0 +1,55 @@ +"use strict" +const { Model } = require("sequelize") +module.exports = (sequelize, DataTypes) => { + class Nickname extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + // eslint-disable-next-line no-unused-vars + static associate(models) { + Nickname.belongsTo(models.User, { + foreignKey: "userId", + as: "user" + }) + Nickname.belongsTo(models.User, { + foreignKey: "friendId", + as: "userNickname" + }) + } + } + Nickname.init( + { + userId: { + allowNull: false, + type: DataTypes.BIGINT + }, + nickname: { + allowNull: false, + type: DataTypes.STRING, + validate: { + notEmpty: true, + len: [1, 20] + } + }, + friendId: { + allowNull: false, + type: DataTypes.BIGINT + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE + } + }, + { + sequelize, + modelName: "Nickname" + } + ) + return Nickname +} diff --git a/backend/models/users.js b/backend/models/users.js index 79e0cd6..6d59f94 100644 --- a/backend/models/users.js +++ b/backend/models/users.js @@ -16,6 +16,10 @@ module.exports = (sequelize, DataTypes) => { as: "friends", foreignKey: "userId" }) + User.hasOne(models.Nickname, { + foreignKey: "friendId", + as: "nickname" + }) } } User.init( @@ -135,6 +139,11 @@ module.exports = (sequelize, DataTypes) => { avatar: { type: DataTypes.STRING, allowNull: true + }, + bot: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false } }, { diff --git a/backend/routes/communications.js b/backend/routes/communications.js index 463160a..629aa32 100644 --- a/backend/routes/communications.js +++ b/backend/routes/communications.js @@ -8,7 +8,8 @@ const { ChatAssociation, Message, Friend, - Attachment + Attachment, + Nickname } = require("../models") const { Op } = require("sequelize") const rateLimit = require("express-rate-limit") @@ -57,25 +58,62 @@ router.get("/", auth, async (req, res, next) => { { model: Chat, as: "chat", + attributes: [ + "id", + "name", + "updatedAt", + "createdAt", + "userId", + "type" + ], include: [ + { + model: User, + as: "users", + attributes: ["username", "avatar", "id", "status", "bot"], + include: [ + { + model: Nickname, + as: "nickname", + required: false, + attributes: ["nickname"], + where: { + userId: req.user.id + } + } + ] + }, { model: ChatAssociation, as: "associations", - attributes: { - exclude: ["lastRead"] - }, + order: [["lastRead", "DESC"]], include: [ { model: User, as: "user", attributes: [ "username", - "name", "avatar", "id", "createdAt", "updatedAt", - "status" + + "status", + + "admin", + "status", + "bot" + ], + include: [ + { + model: Nickname, + as: "nickname", + attributes: ["nickname"], + required: false, + where: { + userId: req.user.id + } + } ] } ] @@ -83,42 +121,136 @@ router.get("/", auth, async (req, res, next) => { { model: Message, as: "lastMessages", - limit: 50, + limit: 30, order: [["id", "DESC"]], - attributes: ["id", "content", "createdAt", "updatedAt", "userId"] - }, - { - model: User, - as: "users", - attributes: [ - "username", - "name", - "avatar", - "id", - "createdAt", - "updatedAt", - "status" - ] + attributes: ["id", "createdAt", "updatedAt", "userId"] } ] }, { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt", "status"], + include: [ + { + model: Nickname, + as: "nickname", + attributes: ["nickname"], + required: false, + where: { + userId: req.user.id + } + } + ] + } + ] + }) + chats.sort((a, b) => { + if (a.chat.lastMessages.length > 0 && b.chat.lastMessages.length > 0) { + return b.chat.lastMessages[0].id - a.chat.lastMessages[0].id + } else if (a.chat.lastMessages.length > 0) { + return -1 + } else if (b.chat.lastMessages.length > 0) { + return 1 + } else { + return b.chat.id - a.chat.id + } + }) + res.json( + chats.map((chat) => { + return { + ...chat.dataValues, + unread: chat.chat.lastMessages.filter( + (message) => message.id > chat.lastRead + ).length, + chat: { + ...chat.chat.dataValues, + lastMessages: null + } + } + }) + ) + } catch (err) { + next(err) + } +}) + +router.get("/mutual/:id/friends", auth, async (req, res, next) => { + try { + const userFriends = await Friend.findAll({ + where: { + userId: req.params.id + } + }) + const friends = await Friend.findAll({ + where: { + friendId: userFriends.map((friend) => friend.friendId), + userId: req.user.id + }, + include: [ + { + model: User, + as: "user2", + attributes: [ + "id", + "username", + "avatar", + + "createdAt", + "updatedAt", + + "admin", + + "status" + ], + include: [ + { + model: Nickname, + as: "nickname", + attributes: ["nickname"], + required: false, + where: { + userId: req.user.id + } + } + ] + } + ] + }) + res.json(friends.map((friend) => friend.user2)) + } catch (err) { + next(err) + } +}) + +router.get("/mutual/:id/groups", auth, async (req, res, next) => { + try { + const userGroups = await ChatAssociation.findAll({ + where: { + userId: req.params.id + } + }) + // find all groups that the req.params.id is a member of and that the req.user.id is a member of + const groups = await ChatAssociation.findAll({ + where: { + userId: req.user.id, + chatId: userGroups.map((group) => group.chatId) + }, + include: [ + { + model: Chat, + as: "chat", + where: { + type: "group" + } } ] }) res.json( - chats.sort((a, b) => { - if (a.chat.lastMessages.length > 0 && b.chat.lastMessages.length > 0) { - return b.chat.lastMessages[0].id - a.chat.lastMessages[0].id - } else if (a.chat.lastMessages.length > 0) { - return -1 - } else if (b.chat.lastMessages.length > 0) { - return 1 - } else { - return b.chat.id - a.chat.id + groups.map((group) => { + return { + ...group.dataValues.chat.dataValues, + associationId: group.id } }) ) @@ -132,6 +264,7 @@ router.delete( auth, async (req, res, next) => { try { + const io = req.app.get("io") const chat = await ChatAssociation.findOne({ where: { id: req.params.id, @@ -146,7 +279,7 @@ router.delete( { model: User, as: "users", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] } @@ -156,7 +289,14 @@ router.delete( where: { id: req.params.associationId, chatId: chat.chat.id - } + }, + include: [ + { + model: User, + as: "user", + attributes: ["id", "username", "createdAt", "updatedAt"] + } + ] }) if (!chat) { throw Errors.chatNotFoundOrNotAdmin @@ -166,6 +306,100 @@ router.delete( } await association.destroy() res.sendStatus(204) + const message = await Message.create({ + userId: 0, + chatId: chat.chat.id, + content: `${association.user.username} has been removed by ${req.user.username}.`, + type: "leave" + }) + const associations = await ChatAssociation.findAll({ + where: { + chatId: chat.chat.id + }, + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "name", + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }) + const messageLookup = await Message.findOne({ + where: { + id: message.id + }, + include: [ + { + model: Attachment, + as: "attachments" + }, + { + model: Message, + as: "reply", + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }, + { + model: Chat, + as: "chat", + include: [ + { + model: User, + as: "users", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }, + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }) + associations.forEach((association) => { + io.to(association.userId).emit("message", { + ...messageLookup.dataValues, + associationId: association.id, + keyId: `${message.id}-${message.updatedAt.toISOString()}` + }) + }) } catch (err) { next(err) } @@ -187,7 +421,7 @@ router.put("/association/:id/:associationId", auth, async (req, res, next) => { { model: User, as: "users", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] } @@ -234,7 +468,7 @@ router.post("/association/:id", auth, async (req, res, next) => { { model: User, as: "users", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] } @@ -284,6 +518,26 @@ router.post("/association/:id", auth, async (req, res, next) => { "One or more users are already in this chat" ) } + const associations = await ChatAssociation.findAll({ + where: { + chatId: chat.chatId + }, + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }) for (let i = 0; i < req.body.users.length; i++) { const c1 = await ChatAssociation.create({ chatId: chat.chat.id, @@ -305,11 +559,12 @@ router.post("/association/:id", auth, async (req, res, next) => { attributes: [ "username", "name", + "avatar", "id", "createdAt", "updatedAt", - , + "status" ] } @@ -318,11 +573,86 @@ router.post("/association/:id", auth, async (req, res, next) => { { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] }) io.to(req.body.users[i]).emit("chatAdded", association) + const message = await Message.create({ + userId: 0, + chatId: chat.chatId, + content: `${association.user.username} has been added by ${req.user.username}.`, + type: "join" + }) + const messageLookup = await Message.findOne({ + where: { + id: message.id + }, + include: [ + { + model: Attachment, + as: "attachments" + }, + { + model: Message, + as: "reply", + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }, + { + model: Chat, + as: "chat", + include: [ + { + model: User, + as: "users", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }, + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }) + associations.forEach((association) => { + io.to(association.userId).emit("message", { + ...messageLookup.dataValues, + associationId: association.id, + keyId: `${message.id}-${message.updatedAt.toISOString()}` + }) + }) } res.sendStatus(204) } else { @@ -343,26 +673,12 @@ router.get("/friends", auth, async (req, res, next) => { { model: User, as: "user", - attributes: [ - "id", - "username", - "name", - "avatar", - "createdAt", - "updatedAt" - ] + attributes: ["id", "username", "avatar", "createdAt", "updatedAt"] }, { model: User, as: "user2", - attributes: [ - "id", - "username", - "name", - "avatar", - "createdAt", - "updatedAt" - ] + attributes: ["id", "username", "avatar", "createdAt", "updatedAt"] } ] }) @@ -383,23 +699,10 @@ router.get("/users", auth, async (req, res, next) => { "avatar", "createdAt", "updatedAt", + "status", "admin" - ], - where: { - [Op.or]: [ - { - privacy: { - communications: { - enabled: true - } - } - }, - { - admin: true - } - ] - } + ] }) res.json(users) } catch (err) { @@ -511,12 +814,12 @@ router.put("/friends/:id", auth, async (req, res, next) => { { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] }, { model: User, as: "user2", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] }) @@ -549,12 +852,12 @@ router.get("/search", auth, async (req, res, next) => { { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] }, { model: User, as: "user2", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] }) @@ -608,7 +911,7 @@ router.get("/:id", auth, async (req, res, next) => { { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] }) @@ -726,7 +1029,18 @@ router.put("/:id/message/edit", auth, async (req, res, next) => { { model: User, as: "users", - attributes: ["id"] + attributes: ["id"], + include: [ + { + model: Nickname, + as: "nickname", + required: false, + attributes: ["nickname"], + where: { + userId: req.user.id + } + } + ] }, { model: Message, @@ -872,6 +1186,7 @@ router.get("/:id/search", auth, async (req, res, next) => { router.delete("/association/:id", auth, async (req, res, next) => { try { + const io = req.app.get("io") const chat = await ChatAssociation.findOne({ where: { userId: req.user.id, @@ -881,6 +1196,101 @@ router.delete("/association/:id", auth, async (req, res, next) => { if (chat) { await chat.destroy() res.sendStatus(204) + const message = await Message.create({ + userId: 0, + chatId: chat.chatId, + content: `${req.user.username} has left the group.`, + type: "leave" + }) + const associations = await ChatAssociation.findAll({ + where: { + chatId: chat.chatId + }, + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }) + const messageLookup = await Message.findOne({ + where: { + id: message.id + }, + include: [ + { + model: Attachment, + as: "attachments" + }, + { + model: Message, + as: "reply", + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }, + { + model: Chat, + as: "chat", + include: [ + { + model: User, + as: "users", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }, + { + model: User, + as: "user", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt" + ] + } + ] + }) + associations.forEach((association) => { + io.to(association.userId).emit("message", { + ...messageLookup.dataValues, + associationId: association.id, + keyId: `${message.id}-${message.updatedAt.toISOString()}` + }) + }) } else { throw Errors.invalidParameter("chat association id") } @@ -1053,6 +1463,17 @@ router.post( "id", "createdAt", "updatedAt" + ], + include: [ + { + model: Nickname, + as: "nickname", + required: false, + attributes: ["nickname"], + where: { + userId: req.user.id + } + } ] } ] @@ -1087,16 +1508,24 @@ router.post( "id", "createdAt", "updatedAt" + ], + include: [ + { + model: Nickname, + as: "nickname", + required: false, + attributes: ["nickname"], + where: { + userId: req.user.id + } + } ] } ] }) const associations = await ChatAssociation.findAll({ where: { - chatId: chat.chat.id, - userId: { - [Op.ne]: req.user.id - } + chatId: chat.chat.id }, include: [ { @@ -1110,17 +1539,38 @@ router.post( "id", "createdAt", "updatedAt" + ], + include: [ + { + model: Nickname, + as: "nickname", + required: false, + attributes: ["nickname"], + where: { + userId: req.user.id + } + } ] } ] }) - associations.forEach((association) => { + for (const association of associations) { io.to(association.userId).emit("message", { ...messageLookup.dataValues, + user: { + ...messageLookup.user.dataValues, + nickname: await Nickname.findOne({ + where: { + userId: association.userId, + friendId: messageLookup.dataValues.user.dataValues.id + }, + attributes: ["nickname"] + }) + }, associationId: association.id, keyId: `${message.id}-${message.updatedAt.toISOString()}` }) - }) + } res.json({ ...messageLookup.dataValues, keyId: `${message.id}-${message.updatedAt.toISOString()}` @@ -1189,7 +1639,9 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" ] } ] @@ -1200,10 +1652,13 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { attributes: [ "username", "name", + "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" ] } ] @@ -1223,12 +1678,18 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { throw Errors.invalidParameter("reply id") } } + let embeds + if (req.user.bot) { + embeds = req.body.embeds + } else { + embeds = [] + } const message = await Message.create({ content: req.body.message, userId: req.user.id, chatId: chat.chat.id, attachments: [], - embeds: [], + embeds, replyId: reply.id }) const messageLookup = await Message.findOne({ @@ -1254,7 +1715,20 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" + ], + include: [ + { + model: Nickname, + as: "nickname", + required: false, + attributes: ["nickname"], + where: { + userId: req.user.id + } + } ] }, { @@ -1277,7 +1751,9 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" ] } ] @@ -1292,17 +1768,27 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" + ], + include: [ + { + model: Nickname, + as: "nickname", + required: false, + attributes: ["nickname"], + where: { + userId: req.user.id + } + } ] } ] }) const associations = await ChatAssociation.findAll({ where: { - chatId: chat.chat.id, - userId: { - [Op.ne]: req.user.id - } + chatId: chat.chat.id }, include: [ { @@ -1315,7 +1801,9 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" ] } ] @@ -1325,13 +1813,23 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { chatId: chat.chat.id } }) - associations.forEach((association) => { + for (const association of associations) { io.to(association.userId).emit("message", { ...messageLookup.dataValues, + user: { + ...messageLookup.user.dataValues, + nickname: await Nickname.findOne({ + where: { + userId: association.userId, + friendId: messageLookup.dataValues.user.dataValues.id + }, + attributes: ["nickname"] + }) + }, associationId: association.id, keyId: `${message.id}-${message.updatedAt.toISOString()}` }) - }) + } res.json({ ...messageLookup.dataValues, keyId: `${message.id}-${message.updatedAt.toISOString()}` @@ -1356,6 +1854,58 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { } }) +router.post("/nickname/:id", auth, async (req, res, next) => { + try { + const user = await User.findOne({ + where: { + id: req.params.id + } + }) + if (!user) { + throw Errors.invalidParameter("user id") + } + if (!req.body.nickname?.length) { + await Nickname.destroy({ + where: { + userId: req.user.id, + friendId: req.params.id + } + }) + res.json({ + nickname: user.username, + userId: user.id + }) + return + } + if (req.body.nickname.length > 20) { + throw Errors.invalidParameter("nickname", "Maximum length is 20") + } + const nickname = await Nickname.findOne({ + where: { + userId: req.user.id, + friendId: req.params.id + } + }) + if (nickname) { + await nickname.update({ + nickname: req.body.nickname + }) + res.json({ + nickname: req.body.nickname + }) + } else { + const nickname = await Nickname.create({ + userId: req.user.id, + friendId: req.params.id, + nickname: req.body.nickname + }) + res.json(nickname) + } + } catch (err) { + next(err) + } +}) + router.get("/:id/messages", auth, async (req, res, next) => { try { const chat = await ChatAssociation.findOne({ @@ -1378,7 +1928,10 @@ router.get("/:id/messages", auth, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "admin", + "bot" ] } ] @@ -1386,7 +1939,7 @@ router.get("/:id/messages", auth, async (req, res, next) => { { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] }) @@ -1410,7 +1963,20 @@ router.get("/:id/messages", auth, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" + ], + include: [ + { + model: Nickname, + as: "nickname", + attributes: ["nickname"], + required: false, + where: { + userId: req.user.id + } + } ] }, { @@ -1427,7 +1993,9 @@ router.get("/:id/messages", auth, async (req, res, next) => { "avatar", "id", "createdAt", - "updatedAt" + "updatedAt", + + "bot" ] }, { @@ -1487,7 +2055,7 @@ router.put("/:id/typing", auth, async (req, res, next) => { { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] }) @@ -1549,6 +2117,34 @@ router.post("/create", auth, async (req, res, next) => { status: "accepted" } }) + if (type === "direct") { + const chats = await Chat.findAll({ + where: { + userId: req.user.id, + type: "direct" + }, + include: [ + { + model: ChatAssociation, + as: "associations" + } + ] + }) + const chat = chats.find((chat) => { + return chat.associations.find((association) => { + return association.userId === req.body.users[0] + }) + }) + if (chat) { + res.json({ + ...chat.associations.find((association) => { + return association.userId === req.user.id + }).dataValues, + existing: true + }) + return + } + } if (friends.length !== req.body.users.length) { throw Errors.invalidParameter( "User", @@ -1586,6 +2182,39 @@ router.post("/create", auth, async (req, res, next) => { model: Chat, as: "chat", include: [ + { + model: ChatAssociation, + as: "associations", + order: [["lastRead", "DESC"]], + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "avatar", + "id", + "createdAt", + "updatedAt", + + "status", + + "admin" + ], + include: [ + { + model: Nickname, + as: "nickname", + attributes: ["nickname"], + required: false, + where: { + userId: req.user.id + } + } + ] + } + ] + }, { model: User, as: "users", @@ -1597,6 +2226,7 @@ router.post("/create", auth, async (req, res, next) => { "id", "createdAt", "updatedAt", + "status" ] } @@ -1605,13 +2235,80 @@ router.post("/create", auth, async (req, res, next) => { { model: User, as: "user", - attributes: ["id", "username", "name", "createdAt", "updatedAt"] + attributes: ["id", "username", "createdAt", "updatedAt"] } ] }) io.to(req.body.users[i]).emit("chatAdded", association) } - res.json(chat) + const association = await ChatAssociation.findOne({ + where: { + userId: req.user.id, + chatId: chat.id + }, + include: [ + { + model: Chat, + as: "chat", + include: [ + { + model: ChatAssociation, + as: "associations", + order: [["lastRead", "DESC"]], + include: [ + { + model: User, + as: "user", + attributes: [ + "username", + "avatar", + "id", + "createdAt", + "updatedAt", + + "status", + + "admin" + ], + include: [ + { + model: Nickname, + as: "nickname", + attributes: ["nickname"], + required: false, + where: { + userId: req.user.id + } + } + ] + } + ] + }, + { + model: User, + as: "users", + attributes: [ + "username", + "name", + + "avatar", + "id", + "createdAt", + "updatedAt", + + "status" + ] + } + ] + }, + { + model: User, + as: "user", + attributes: ["id", "username", "createdAt", "updatedAt"] + } + ] + }) + res.json(association) } catch (err) { next(err) } diff --git a/backend/routes/user.js b/backend/routes/user.js index b08b8aa..52e89fb 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.js @@ -342,40 +342,6 @@ router.put("/settings/:type", auth, async (req, res, next) => { return false } } - async function checkPassword(password) { - return new Promise((resolve, reject) => { - axios - .post( - "http://localhost:23994/services/admin.svc/GetApiKey", - { - password: password, - schoolId: req.body.schoolId, - sussiId: req.user.sussiId - }, - { - headers: { - compassInstance: - req.header("compassInstance") || - req.query.compassInstance || - "devices" - } - } - ) - .then((resp) => { - if (resp.data.d) { - resolve(true) - return true - } else { - resolve(false) - return false - } - }) - .catch((e) => { - reject(e) - return false - }) - }) - } try { const io = req.app.get("io") if (req.params.type === "full") { @@ -452,66 +418,6 @@ router.put("/settings/:type", auth, async (req, res, next) => { } else { throw Errors.invalidParameter("Password or Code") } - } else if (req.params.type === "totpConfirm") { - const user = await User.findOne({ - where: { - id: req.user.id - } - }) - if (user.totp) { - const verified = speakeasy.totp.verify({ - secret: user.totp, - encoding: "base32", - token: req.body.code - }) - if (verified) { - await User.update( - { - totpEnabled: true - }, - { - where: { - id: req.user.id - } - } - ) - res.sendStatus(204) - } else { - throw Errors.invalidTotp - } - } else { - throw Errors.unknown - } - } else if (req.params.type === "bcSessionsEnable") { - /* - const match = await checkPassword(req.body.password) - if (match) { - await User.update( - { - bcSessionsEnabled: true - }, - { - where: { - id: req.user.id - } - } - ) - const session = await Session.create({ - session: - "BETTERCOMPASS-" + - cryptoRandomString({ - length: 64 - }), - compassUserId: req.user.compassUserId, - sussiId: req.user.sussiId, - instance: req.user.instance, - other: {}, - userId: req.user.id - }) - } else { - throw Errors.invalidCredentials - }*/ - throw Errors.experimentsOptIn } else if (req.params.type === "password") { const user = await User.findOne({ where: { diff --git a/package.json b/package.json index d46fcd0..0793a13 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@mdi/js": "^6.6.95", "@sentry/tracing": "^6.19.3", "@sentry/vue": "^6.19.3", + "@tauri-apps/api": "^1.0.2", "apollo-boost": "^0.4.9", "axios": "^0.26.1", "brace": "^0.11.1", diff --git a/src/App.vue b/src/App.vue index 93d285e..1c60a6f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -404,27 +404,6 @@
- - - Introducing Compact Mode, a better experience for - lower resolution devices. - - Settings - - - + + diff --git a/src/components/Header.vue b/src/components/Header.vue index 12026f5..3c8a01f 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -3,6 +3,39 @@ + + + + + Change Friend Nickname + + + Group Settings + + + Leave Group + + + + + + + + @@ -293,148 +353,147 @@ - + - - mdi-account-multiple - Friends - - - - - CHATS ({{ $store.state.chats.length }}) - - - - mdi-plus - - - - - - - +