Colubrina/backend/routes/communications.js

2115 lines
51 KiB
JavaScript
Raw Normal View History

2022-06-05 22:58:18 +10:00
const express = require("express")
const router = express.Router()
const Errors = require("../lib/errors.js")
const auth = require("../lib/authorize.js")
const {
User,
Chat,
ChatAssociation,
2022-07-31 18:42:36 +10:00
Pin,
2022-06-05 22:58:18 +10:00
Message,
Friend,
2022-07-29 01:12:29 +10:00
Attachment,
2022-08-14 22:06:56 +10:00
Nickname,
Poll,
PollAnswer
2022-06-05 22:58:18 +10:00
} = require("../models")
const { Op } = require("sequelize")
const rateLimit = require("express-rate-limit")
const multer = require("multer")
const cryptoRandomString = require("crypto-random-string")
const path = require("path")
const fs = require("fs")
const FileType = require("file-type")
2022-08-14 22:06:56 +10:00
const { v4: uuidv4 } = require("uuid")
2022-06-05 22:58:18 +10:00
const limiter = rateLimit({
windowMs: 10 * 1000,
max: 8,
message: Errors.rateLimit,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => req.user.id || req.ip
})
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "usercontent/")
},
filename: function (req, file, cb) {
cb(
null,
cryptoRandomString({ length: 32 }) + path.extname(file.originalname)
)
}
})
const upload = multer({
storage: storage,
2022-08-14 22:06:56 +10:00
limits: { fileSize: 50 * 1024 * 1024 }
2022-06-05 22:58:18 +10:00
})
const resolveEmbeds = require("../lib/resolveEmbeds.js")
const paginate = require("jw-paginate")
2022-07-29 19:20:19 +10:00
2022-07-29 19:04:37 +10:00
async function createMessage(req, type, content, association, userId) {
const io = req.app.get("io")
const message = await Message.create({
userId: 0,
chatId: association.chatId,
content: content,
type: type || "system"
})
const associations = await ChatAssociation.findAll({
where: {
chatId: association.chatId
}
})
const messageLookup = await Message.findOne({
where: {
id: message.id
},
include: [
2022-07-31 18:42:36 +10:00
{
model: ChatAssociation,
as: "readReceipts",
attributes: ["id"],
include: [
{
model: User,
as: "user",
attributes: ["username", "name", "avatar", "id"]
}
]
},
2022-07-29 19:04:37 +10:00
{
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((user) => {
io.to(user.dataValues.userId).emit("message", {
...messageLookup.dataValues,
associationId: user.dataValues.id,
keyId: `${
message.dataValues.id
2022-08-06 19:22:21 +10:00
}-${message.dataValues.updatedAt.toISOString()}`,
notify:
association.notifications === "all" ||
(association.notifications === "mentions" &&
message.content
.toLowerCase()
.includes(association.user.username.toLowerCase()))
2022-07-29 19:04:37 +10:00
})
})
}
2022-07-29 19:20:19 +10:00
2022-07-31 14:56:43 +10:00
router.all("*", auth, async (req, res, next) => {
try {
2022-08-07 00:35:00 +10:00
if (!req.user.emailVerified && req.app.locals.config.emailVerification) {
2022-07-31 14:56:43 +10:00
throw Errors.emailVerificationRequired
} else {
next()
}
} catch (e) {
next(e)
}
})
2022-06-05 22:58:18 +10:00
router.get("/", auth, async (req, res, next) => {
try {
let chats = await ChatAssociation.findAll({
where: {
userId: req.user.id
},
include: [
{
model: Chat,
as: "chat",
2022-07-29 01:12:29 +10:00
attributes: [
"id",
"name",
"updatedAt",
"createdAt",
"userId",
"type"
],
2022-06-05 22:58:18 +10:00
include: [
2022-07-29 01:12:29 +10:00
{
model: User,
as: "users",
attributes: ["username", "avatar", "id", "status", "bot"],
include: [
{
model: Nickname,
as: "nickname",
required: false,
attributes: ["nickname"],
where: {
userId: req.user.id
}
}
]
},
2022-06-05 22:58:18 +10:00
{
model: ChatAssociation,
as: "associations",
2022-07-29 01:12:29 +10:00
order: [["lastRead", "DESC"]],
2022-06-05 22:58:18 +10:00
include: [
{
model: User,
as: "user",
attributes: [
"username",
"avatar",
"id",
"createdAt",
"updatedAt",
2022-07-29 01:12:29 +10:00
"status",
"admin",
"status",
"bot"
],
include: [
{
model: Nickname,
as: "nickname",
attributes: ["nickname"],
required: false,
where: {
userId: req.user.id
}
}
2022-06-05 22:58:18 +10:00
]
}
]
},
{
model: Message,
as: "lastMessages",
2022-07-29 01:12:29 +10:00
limit: 30,
2022-06-05 22:58:18 +10:00
order: [["id", "DESC"]],
2022-07-29 01:12:29 +10:00
attributes: ["id", "createdAt", "updatedAt", "userId"]
2022-06-05 22:58:18 +10:00
}
]
},
{
model: User,
as: "user",
2022-07-29 01:12:29 +10:00
attributes: ["id", "username", "createdAt", "updatedAt", "status"],
include: [
{
model: Nickname,
as: "nickname",
attributes: ["nickname"],
required: false,
where: {
userId: req.user.id
}
}
]
2022-06-05 22:58:18 +10:00
}
]
})
2022-07-29 01:12:29 +10:00
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
}
})
2022-06-05 22:58:18 +10:00
res.json(
2022-07-29 01:12:29 +10:00
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(
groups.map((group) => {
return {
...group.dataValues.chat.dataValues,
associationId: group.id
2022-06-05 22:58:18 +10:00
}
})
)
} catch (err) {
next(err)
}
})
router.get("/users", auth, async (req, res, next) => {
try {
2022-08-07 00:35:00 +10:00
if (req.app.locals.config.publicUsers) {
2022-07-30 17:45:29 +10:00
const users = await User.findAll({
attributes: [
"id",
"username",
"name",
"avatar",
"createdAt",
"updatedAt",
"status",
"admin"
],
where: {
banned: false
}
})
res.json(users)
} else {
res.json([])
}
2022-06-05 22:58:18 +10:00
} catch (err) {
next(err)
}
})
router.get("/search", auth, async (req, res, next) => {
try {
const friends = await Friend.findAll({
where: {
userId: req.user.id,
status: "accepted"
},
include: [
{
model: User,
as: "user",
2022-07-29 01:12:29 +10:00
attributes: ["id", "username", "createdAt", "updatedAt"]
2022-06-05 22:58:18 +10:00
},
{
model: User,
as: "user2",
2022-07-29 01:12:29 +10:00
attributes: ["id", "username", "createdAt", "updatedAt"]
2022-06-05 22:58:18 +10:00
}
]
})
const users = await User.findAll({
where: {
id: friends.map((friend) => friend.friendId),
username: {
[Op.like]: `%${req.query.query}%`
}
},
attributes: ["username", "name", "avatar", "id", "createdAt", "updatedAt"]
})
users.forEach((user) => {
if (user.id === req.user.id) {
users.splice(users.indexOf(user), 1)
}
})
res.json(users)
} catch (err) {
next(err)
}
})
router.get("/:id", auth, async (req, res, next) => {
try {
let chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
]
}
]
},
{
model: User,
as: "user",
2022-07-29 01:12:29 +10:00
attributes: ["id", "username", "createdAt", "updatedAt"]
2022-06-05 22:58:18 +10:00
}
]
})
if (chat) {
res.json(chat)
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
2022-07-31 18:42:36 +10:00
router.get("/:id/pins", auth, async (req, res, next) => {
try {
let chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat"
}
]
})
if (chat) {
const pins = await Pin.findAll({
where: {
chatId: chat.chat.id
},
include: [
{
model: User,
as: "pinnedBy",
required: false,
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
],
include: [
{
model: Nickname,
as: "nickname",
attributes: ["nickname"],
required: false,
where: {
userId: req.user.id
}
}
]
},
{
model: Message,
as: "message",
2022-08-31 18:46:40 +10:00
required: true,
2022-07-31 18:42:36 +10:00
include: [
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
],
include: [
{
model: Nickname,
as: "nickname",
attributes: ["nickname"],
required: false,
where: {
userId: req.user.id
}
}
]
}
]
}
],
order: [["id", "DESC"]]
})
res.json(
pins.map((pin) => {
const message = pin.dataValues.message.dataValues
return {
...pin.dataValues,
message: {
...pin.dataValues.message.dataValues,
keyId: `${message.id}-${message.updatedAt.toISOString()}`
}
}
})
)
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
router.post("/:id/pins", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: ["id"]
}
]
}
]
})
if (chat?.chat?.type === "direct" || chat?.rank === "admin") {
const message = await Message.findOne({
where: {
id: req.body.messageId,
chatId: chat.chat.id
}
})
if (message) {
const checkPin = await Pin.findOne({
where: {
messageId: message.id,
chatId: chat.chat.id
}
})
if (checkPin) {
await checkPin.destroy()
res.json({
message: "Message unpinned successfully."
})
2022-07-31 20:10:41 +10:00
await createMessage(
req,
"pin",
`${req.user.username} unpinned a message from the chat.`,
chat,
req.user.id
2022-07-31 20:16:44 +10:00
)
2022-07-31 18:42:36 +10:00
return
}
const pin = await Pin.create({
chatId: chat.chat.id,
messageId: req.body.messageId,
pinnedById: req.user.id
})
await createMessage(
req,
"pin",
`${req.user.username} pinned a message to the chat.`,
chat,
req.user.id
)
res.json({
...pin.dataValues,
message: "Message pinned successfully."
})
}
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (e) {
next(e)
}
})
2022-06-05 22:58:18 +10:00
router.put("/:id/read", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
2022-07-31 18:42:36 +10:00
{
model: User,
as: "users",
attributes: ["id"]
},
2022-06-05 22:58:18 +10:00
{
model: Message,
as: "lastMessages",
limit: 50,
order: [["id", "DESC"]],
attributes: ["id", "content", "createdAt", "updatedAt"]
}
]
}
]
})
if (chat) {
2022-07-31 20:16:44 +10:00
if (req.user.storedStatus !== "invisible") {
await chat.update({
lastRead: chat.chat.lastMessages[0]?.id || null
})
io.to(req.user.id).emit("readChat", {
2022-07-31 18:42:36 +10:00
id: chat.id,
2022-07-31 20:16:44 +10:00
lastRead: chat.chat.lastMessages[0]?.id || null
})
res.sendStatus(204)
for (const user of chat.chat.users) {
io.to(user.id).emit("readReceipt", {
id: chat.id,
messageId: chat.chat.lastMessages[0]?.id || null,
userId: req.user.id,
chatId: chat.chat.id,
user: {
username: req.user.username,
avatar: req.user.avatar,
id: req.user.id
},
previousMessageId: chat.lastRead
})
}
} else {
io.to(req.user.id).emit("readChat", {
id: chat.id,
lastRead: chat.chat.lastMessages[0]?.id || null
2022-07-31 18:42:36 +10:00
})
2022-07-31 20:16:44 +10:00
res.sendStatus(204)
2022-07-31 18:42:36 +10:00
}
2022-06-05 22:58:18 +10:00
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
router.put("/:id", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
const association = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id,
rank: "admin"
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: ["id"]
}
]
}
]
})
if (association) {
const chat = await Chat.findOne({
where: {
id: association.chatId
}
})
await chat.update({
name: req.body.name
})
association.chat.users.forEach((user) => {
io.to(user.id).emit("chatUpdated", {
...chat.dataValues,
name: req.body.name
})
})
2022-07-29 19:04:37 +10:00
await createMessage(
req,
"rename",
`${req.user.username} renamed the chat to ${req.body.name}`,
association,
req.user.id
)
2022-06-05 22:58:18 +10:00
res.sendStatus(204)
} else {
throw Errors.chatNotFoundOrNotAdmin
}
} catch (err) {
next(err)
}
})
2022-08-06 19:22:21 +10:00
router.put("/settings/:id", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
const association = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
}
})
if (association) {
await association.update({
notifications: req.body.notifications
})
res.sendStatus(204)
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
2022-06-05 22:58:18 +10:00
router.put("/:id/message/edit", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
2022-07-29 01:12:29 +10:00
attributes: ["id"],
include: [
{
model: Nickname,
as: "nickname",
required: false,
attributes: ["nickname"],
where: {
userId: req.user.id
}
}
]
2022-06-05 22:58:18 +10:00
},
{
model: Message,
as: "lastMessages",
limit: 50,
order: [["id", "DESC"]],
attributes: ["id", "content", "createdAt", "updatedAt"]
}
]
}
]
})
if (chat) {
const message = await Message.findOne({
where: {
id: req.body.id,
chatId: chat.chat.id,
userId: req.user.id
}
})
if (message) {
await message.update({
content: req.body.content,
edited: true,
editedAt: new Date().toISOString()
})
chat.chat.users.forEach((user) => {
io.to(user.id).emit("editMessage", {
chatId: chat.chat.id,
id: message.id,
edited: true,
editedAt: new Date().toISOString(),
content: req.body.content
})
})
res.sendStatus(204)
const associationsWithUser = await ChatAssociation.findAll({
where: {
chatId: chat.chat.id
}
})
await resolveEmbeds(req, message, associationsWithUser)
.then((embeds) => {
associationsWithUser.forEach((association) => {
io.to(association.userId).emit("messageEmbedResolved", {
chatId: chat.chat.id,
id: message.id,
embeds: embeds,
associationId: association.id,
keyId: `${message.id}-${message.updatedAt.toISOString()}-embed`
})
})
})
.catch(() => {})
} else {
throw Errors.invalidParameter("message id")
}
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
router.get("/:id/search", auth, async (req, res, next) => {
try {
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
}
})
if (chat) {
const messages = await Message.findAll({
where: {
chatId: chat.chatId,
content: {
[Op.like]: `%${req.query.query}%`
}
},
order: [["id", "DESC"]],
include: [
{
model: Attachment,
as: "attachments"
},
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
]
},
{
model: Message,
as: "reply",
include: [
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
]
},
{
model: Attachment,
as: "attachments"
}
]
}
]
})
2022-08-31 18:46:40 +10:00
const page = parseInt(req.query.page) || 1
2022-06-05 22:58:18 +10:00
const pager = paginate(messages.length, page, 15)
const result = messages.slice(pager.startIndex, pager.endIndex + 1)
res.json({
messages: result.map((message) => {
return {
...message.dataValues,
keyId: `${message.id}-${message.updatedAt.toISOString()}`
}
}),
pager: pager
})
}
} catch (err) {
next(err)
}
})
router.delete("/:id/message/:mId", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: ["id"]
}
]
}
]
})
if (chat) {
let options
if (chat.rank === "admin") {
options = {
where: {
id: req.params.mId,
chatId: chat.chat.id
}
2022-06-05 22:58:18 +10:00
}
} else {
options = {
where: {
id: req.params.mId,
chatId: chat.chat.id,
userId: req.user.id
}
}
}
const message = await Message.findOne(options)
2022-06-05 22:58:18 +10:00
if (message) {
await message.destroy()
chat.chat.users.forEach((user) => {
io.to(user.id).emit("deleteMessage", {
chatId: chat.chat.id,
id: message.id
})
})
res.sendStatus(204)
} else {
throw Errors.invalidParameter("message id")
}
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
router.post(
"/:id/formData/message",
auth,
limiter,
upload.single("file"),
async (req, res, next) => {
try {
const io = req.app.get("io")
if (req.body.message.length > 999) {
throw Errors.invalidParameter("message", "Maximum length is 1000")
}
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
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"
]
}
]
})
if (chat) {
let reply = {
id: null
}
if (req.body.replyId) {
reply = await Message.findOne({
where: {
id: req.body.replyId,
chatId: chat.chat.id
}
})
if (!reply) {
fs.unlink(req.file.path, (err) => {
if (err) {
console.log("Multer deletion error")
console.log(err)
}
})
throw Errors.invalidParameter("reply id")
}
}
const meta = await FileType.fromFile(req.file.path)
const message = await Message.create({
content: req.body.message,
userId: req.user.id,
chatId: chat.chat.id,
attachments: [],
embeds: [],
replyId: reply.id
})
await Attachment.create({
userId: req.user.id,
type: "message",
attachment: req.file.filename,
name: req.file.originalname,
extension: meta?.ext || req.file.path.split(".").pop() || "other",
size: req.file.size,
messageId: message.id
})
const messageLookup = await Message.findOne({
where: {
id: message.id
},
include: [
2022-07-31 18:42:36 +10:00
{
model: ChatAssociation,
as: "readReceipts",
attributes: ["id"],
include: [
{
model: User,
as: "user",
attributes: ["username", "name", "avatar", "id"]
}
]
},
2022-06-05 22:58:18 +10:00
{
model: Attachment,
as: "attachments"
},
{
model: Message,
as: "reply",
include: [
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
2022-07-29 01:12:29 +10:00
],
include: [
{
model: Nickname,
as: "nickname",
required: false,
attributes: ["nickname"],
where: {
userId: req.user.id
}
}
2022-06-05 22:58:18 +10:00
]
}
]
},
{
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"
2022-07-29 01:12:29 +10:00
],
include: [
{
model: Nickname,
as: "nickname",
required: false,
attributes: ["nickname"],
where: {
userId: req.user.id
}
}
2022-06-05 22:58:18 +10:00
]
}
]
})
const associations = await ChatAssociation.findAll({
where: {
2022-07-29 01:12:29 +10:00
chatId: chat.chat.id
2022-06-05 22:58:18 +10:00
},
include: [
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
2022-07-29 01:12:29 +10:00
],
include: [
{
model: Nickname,
as: "nickname",
required: false,
attributes: ["nickname"],
where: {
userId: req.user.id
}
}
2022-06-05 22:58:18 +10:00
]
}
]
})
2022-07-29 01:12:29 +10:00
for (const association of associations) {
2022-06-05 22:58:18 +10:00
io.to(association.userId).emit("message", {
...messageLookup.dataValues,
2022-07-29 01:12:29 +10:00
user: {
...messageLookup.user.dataValues,
nickname: await Nickname.findOne({
where: {
userId: association.userId,
friendId: messageLookup.dataValues.user.dataValues.id
},
attributes: ["nickname"]
})
},
2022-06-05 22:58:18 +10:00
associationId: association.id,
2022-08-06 19:22:21 +10:00
keyId: `${message.id}-${message.updatedAt.toISOString()}`,
notify:
association.notifications === "all" ||
(association.notifications === "mentions" &&
message.content
.toLowerCase()
.includes(association.user.username.toLowerCase()))
2022-06-05 22:58:18 +10:00
})
2022-07-29 01:12:29 +10:00
}
2022-06-05 22:58:18 +10:00
res.json({
...messageLookup.dataValues,
keyId: `${message.id}-${message.updatedAt.toISOString()}`
})
const associationsWithUser = await ChatAssociation.findAll({
where: {
chatId: chat.chat.id
}
})
await resolveEmbeds(req, messageLookup, associations)
.then((embeds) => {
associationsWithUser.forEach((association) => {
io.to(association.userId).emit("messageEmbedResolved", {
...message.dataValues,
embeds: embeds,
associationId: association.id,
keyId: `${message.id}-${message.updatedAt.toISOString()}-embed`
})
})
})
.catch(() => {})
} else {
fs.unlink(req.file.path, (err) => {
if (err) {
console.log("Multer deletion error")
console.log(err)
}
})
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
}
)
router.post("/:id/message", auth, limiter, async (req, res, next) => {
try {
const io = req.app.get("io")
if (!req.body.message) {
throw Errors.invalidParameter("message")
}
if (!req.body.message.length) {
throw Errors.invalidParameter("message")
}
if (req.body.message.length > 999) {
throw Errors.invalidParameter("message", "Maximum length is 1000")
}
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
2022-06-05 22:58:18 +10:00
]
}
]
},
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
2022-06-05 22:58:18 +10:00
]
}
]
})
if (chat) {
let reply = {
id: null
}
if (req.body.replyId) {
reply = await Message.findOne({
where: {
id: req.body.replyId,
chatId: chat.chat.id
}
})
if (!reply) {
throw Errors.invalidParameter("reply id")
}
}
2022-07-29 01:12:29 +10:00
let embeds
if (req.user.bot) {
embeds = req.body.embeds
} else {
embeds = []
}
2022-06-05 22:58:18 +10:00
const message = await Message.create({
content: req.body.message,
userId: req.user.id,
chatId: chat.chat.id,
attachments: [],
2022-07-29 01:12:29 +10:00
embeds,
2022-06-05 22:58:18 +10:00
replyId: reply.id
})
2022-08-14 22:06:56 +10:00
if (req.body.embeds?.length && !req.user.bot) {
if (req.body.embeds.length > 1) {
throw Errors.invalidParameter("embeds", "Maximum length is 1")
}
for (const embed of req.body.embeds) {
if (embed.type === "poll-v1") {
if (!embed.title) {
throw Errors.invalidParameter("embeds", "title is required")
}
if (embed.options?.length < 2) {
throw Errors.invalidParameter("embeds", "options is required")
}
if (embed.options.length > 4) {
throw Errors.invalidParameter(
"embeds",
"Maximum length is 4 for options"
)
}
await Poll.create({
title: embed.title,
description: embed.description,
userId: req.user.id,
messageId: message.id,
options: embed.options.map((option) => {
return {
value: option.toString(),
id: uuidv4()
}
})
})
}
}
}
2022-06-05 22:58:18 +10:00
const messageLookup = await Message.findOne({
where: {
id: message.id
},
include: [
2022-08-14 22:06:56 +10:00
{
model: Poll,
as: "poll",
include: [
{
model: PollAnswer,
as: "answers",
include: [
{
model: User,
as: "user",
attributes: ["username", "avatar", "id"]
}
]
}
]
},
2022-07-31 18:42:36 +10:00
{
model: ChatAssociation,
as: "readReceipts",
attributes: ["id"],
include: [
{
model: User,
as: "user",
attributes: ["username", "name", "avatar", "id"]
}
]
},
2022-06-05 22:58:18 +10:00
{
model: Attachment,
as: "attachments"
},
{
model: Message,
as: "reply",
include: [
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
],
include: [
{
model: Nickname,
as: "nickname",
required: false,
attributes: ["nickname"],
where: {
userId: req.user.id
}
}
2022-06-05 22:58:18 +10:00
]
},
{
model: Attachment,
as: "attachments"
}
]
},
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
2022-06-05 22:58:18 +10:00
]
}
]
},
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
],
include: [
{
model: Nickname,
as: "nickname",
required: false,
attributes: ["nickname"],
where: {
userId: req.user.id
}
}
2022-06-05 22:58:18 +10:00
]
}
]
})
const associations = await ChatAssociation.findAll({
where: {
2022-07-29 01:12:29 +10:00
chatId: chat.chat.id
2022-06-05 22:58:18 +10:00
},
include: [
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
2022-06-05 22:58:18 +10:00
]
}
]
})
const associationsWithUser = await ChatAssociation.findAll({
where: {
chatId: chat.chat.id
}
})
2022-07-29 01:12:29 +10:00
for (const association of associations) {
2022-06-05 22:58:18 +10:00
io.to(association.userId).emit("message", {
...messageLookup.dataValues,
2022-07-29 01:12:29 +10:00
user: {
...messageLookup.user.dataValues,
nickname: await Nickname.findOne({
where: {
userId: association.userId,
friendId: messageLookup.dataValues.user.dataValues.id
},
attributes: ["nickname"]
})
},
2022-06-05 22:58:18 +10:00
associationId: association.id,
2022-08-06 19:22:21 +10:00
keyId: `${message.id}-${message.updatedAt.toISOString()}`,
notify:
association.notifications === "all" ||
(association.notifications === "mentions" &&
message.content
.toLowerCase()
.includes(association.user.username.toLowerCase()))
2022-06-05 22:58:18 +10:00
})
2022-07-29 01:12:29 +10:00
}
2022-06-05 22:58:18 +10:00
res.json({
...messageLookup.dataValues,
keyId: `${message.id}-${message.updatedAt.toISOString()}`
})
await resolveEmbeds(req, messageLookup, associations)
.then((embeds) => {
associationsWithUser.forEach((association) => {
io.to(association.userId).emit("messageEmbedResolved", {
...message.dataValues,
embeds: embeds,
associationId: association.id,
keyId: `${message.id}-${message.updatedAt.toISOString()}-embed`
})
})
})
.catch(() => {})
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
2022-07-29 01:12:29 +10:00
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)
}
})
2022-06-05 22:58:18 +10:00
router.get("/:id/messages", auth, async (req, res, next) => {
try {
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"admin",
"bot"
2022-06-05 22:58:18 +10:00
]
}
]
},
{
model: User,
as: "user",
2022-07-29 01:12:29 +10:00
attributes: ["id", "username", "createdAt", "updatedAt"]
2022-06-05 22:58:18 +10:00
}
]
})
if (chat) {
2022-07-30 17:45:29 +10:00
let or
if (parseInt(req.query.offset)) {
or = {
[Op.or]: [
{
id: {
[Op.lt]: parseInt(req.query.offset)
? parseInt(req.query.offset)
: 0
}
}
]
}
} else {
or = {}
}
2022-06-05 22:58:18 +10:00
const messages = await Message.findAll({
where: {
2022-07-30 17:45:29 +10:00
chatId: chat.chat.id,
...or
2022-06-05 22:58:18 +10:00
},
include: [
2022-08-14 22:06:56 +10:00
{
model: Poll,
as: "poll",
include: [
{
model: PollAnswer,
as: "answers",
include: [
{
model: User,
as: "user",
required: false,
attributes: ["username", "avatar", "id"]
}
]
}
]
},
2022-07-31 18:42:36 +10:00
{
model: ChatAssociation,
as: "readReceipts",
attributes: ["id"],
include: [
{
model: User,
as: "user",
attributes: ["username", "name", "avatar", "id"]
}
]
},
2022-06-05 22:58:18 +10:00
{
model: Attachment,
as: "attachments"
},
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
],
include: [
{
model: Nickname,
as: "nickname",
attributes: ["nickname"],
required: false,
where: {
userId: req.user.id
}
}
2022-06-05 22:58:18 +10:00
]
},
{
model: Message,
as: "reply",
include: [
{
model: User,
as: "user",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
2022-07-29 01:12:29 +10:00
"updatedAt",
"bot"
2022-06-05 22:58:18 +10:00
]
},
{
model: Attachment,
as: "attachments"
}
]
}
],
order: [["id", "DESC"]],
limit: 50
})
const messagesWithKeyId = messages.map((message) => {
return {
...message.dataValues,
keyId: `${message.id}-${message.updatedAt.toISOString()}`
}
})
res.json(messagesWithKeyId.sort((a, b) => a.id - b.id))
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
router.put("/:id/typing", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
const chat = await ChatAssociation.findOne({
where: {
userId: req.user.id,
id: req.params.id
},
include: [
{
model: Chat,
as: "chat",
include: [
{
model: User,
as: "users",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt"
]
}
]
},
{
model: User,
as: "user",
2022-07-29 01:12:29 +10:00
attributes: ["id", "username", "createdAt", "updatedAt"]
2022-06-05 22:58:18 +10:00
}
]
})
if (chat) {
const userIds = chat.chat.users.map((user) => user.id)
const userIdsWithoutCurrentUser = userIds.filter(
(userId) => userId !== req.user.id
)
userIdsWithoutCurrentUser.forEach((userId) => {
const date = new Date()
io.to(userId).emit("typing", {
chatId: chat.chat.id,
userId: req.user.id,
timeout: new Date(date.getTime() + 5000).toISOString(),
date: new Date(date).toISOString(),
username: req.user.username
})
})
res.sendStatus(204)
} else {
throw Errors.invalidParameter("chat association id")
}
} catch (err) {
next(err)
}
})
router.post("/create", auth, async (req, res, next) => {
try {
const io = req.app.get("io")
let name
let type
if (req.body.users.length <= 1) {
name = "Direct Message"
type = "direct"
} else {
name = "Unnamed Group"
type = "group"
}
if (req.body.users.length > 10) {
throw Errors.invalidParameter("User", "The maximum number of users is 10")
}
if (!req.body.users.length) {
throw Errors.invalidParameter(
"User",
"You need at least 1 user to create a chat"
)
}
if (req.body.users.includes(req.user.id)) {
throw Errors.invalidParameter(
"User",
"You cannot create a DM with yourself"
)
}
const friends = await Friend.findAll({
where: {
userId: req.user.id,
friendId: req.body.users,
status: "accepted"
}
})
2022-07-29 01:12:29 +10:00
if (type === "direct") {
2022-07-30 18:38:11 +10:00
const chats = await ChatAssociation.findAll({
2022-07-29 01:12:29 +10:00
where: {
2022-07-30 18:38:11 +10:00
userId: req.user.id
2022-07-29 01:12:29 +10:00
},
include: [
{
2022-07-30 18:38:11 +10:00
model: Chat,
as: "chat",
where: {
type: "direct"
},
include: [
{
model: User,
as: "users"
}
]
2022-07-29 01:12:29 +10:00
}
]
})
const chat = chats.find((chat) => {
2022-07-30 18:38:11 +10:00
const users = chat.chat.users.map((user) => user.id)
return users.includes(req.body.users[0]) && users.includes(req.user.id)
2022-07-29 01:12:29 +10:00
})
if (chat) {
res.json({
2022-07-30 18:38:11 +10:00
...chat.dataValues,
2022-07-29 01:12:29 +10:00
existing: true
})
return
}
}
2022-06-05 22:58:18 +10:00
if (friends.length !== req.body.users.length) {
throw Errors.invalidParameter(
"User",
"You are not friends with this user"
)
}
const chat = await Chat.create({
name,
userId: req.user.id,
type
})
req.body.users.push(req.user.id)
2022-07-30 15:14:26 +10:00
for (const id of req.body.users) {
2022-06-05 22:58:18 +10:00
let rank
if (type === "group") {
2022-07-30 15:14:26 +10:00
if (id === req.user.id) {
2022-06-05 22:58:18 +10:00
rank = "admin"
} else {
rank = "member"
}
} else {
rank = "member"
}
2022-07-30 15:14:26 +10:00
await ChatAssociation.create({
2022-06-05 22:58:18 +10:00
chatId: chat.id,
2022-07-30 15:14:26 +10:00
userId: id,
2022-06-05 22:58:18 +10:00
rank
})
2022-07-30 15:14:26 +10:00
}
for (const id of req.body.users) {
2022-06-05 22:58:18 +10:00
const association = await ChatAssociation.findOne({
where: {
2022-07-30 15:14:26 +10:00
chatId: chat.id,
userId: id
2022-06-05 22:58:18 +10:00
},
include: [
{
model: Chat,
as: "chat",
include: [
2022-07-29 01:12:29 +10:00
{
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
}
}
]
}
]
},
2022-06-05 22:58:18 +10:00
{
model: User,
as: "users",
attributes: [
"username",
"name",
"avatar",
"id",
"createdAt",
"updatedAt",
2022-07-29 01:12:29 +10:00
2022-06-05 22:58:18 +10:00
"status"
]
}
]
},
{
model: User,
as: "user",
2022-07-29 01:12:29 +10:00
attributes: ["id", "username", "createdAt", "updatedAt"]
2022-06-05 22:58:18 +10:00
}
]
})
2022-07-30 15:14:26 +10:00
io.to(id).emit("chatAdded", association)
2022-06-05 22:58:18 +10:00
}
2022-07-29 01:12:29 +10:00
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)
2022-06-05 22:58:18 +10:00
} catch (err) {
next(err)
}
})
module.exports = router