From fb90f1ef4f3f35a2913c7cb7e6633c70d0c01439 Mon Sep 17 00:00:00 2001 From: Lukian Date: Mon, 12 May 2025 11:26:15 +0200 Subject: [PATCH] add: added image attachments to messages --- back/api/attachments.js | 22 ++++++++++++++ back/api/channels.js | 37 ++++++++++++++++++++--- back/api/lastmessages.js | 8 ++++- back/api/messages.js | 14 ++++++++- back/api/users.js | 16 +++++++++- back/libs/mysql.js | 35 +++++++++++++++++++++ front/src/components/MessageComponent.tsx | 7 +++++ front/src/pages/ChannelPage.tsx | 15 ++++++++- front/src/styles/MessageComponent.css | 11 +++++++ front/src/types.tsx | 14 +++++++-- 10 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 back/api/attachments.js diff --git a/back/api/attachments.js b/back/api/attachments.js new file mode 100644 index 0000000..54e865e --- /dev/null +++ b/back/api/attachments.js @@ -0,0 +1,22 @@ +const express = require('express'); +const { getConnection, getAttachment } = require('../libs/mysql'); +const fs = require('node:fs'); +const path = require('node:path'); + +const router = express.Router(); + +router.get('/:file_name', async (req, res) => { + const { file_name } = req.params; + + const connection = await getConnection(); + const attachment = await getAttachment(connection, file_name); + connection.end(); + + if (!attachment[0]) { + return res.status(404).send({ error: 'File not found' }); + } + + res.sendFile(path.join(__dirname, `../data/attachments/${attachment[0].file_name}`), { headers: { 'Content-Type': 'image' } }); +}) + +module.exports = router; \ No newline at end of file diff --git a/back/api/channels.js b/back/api/channels.js index 6dbceab..d786380 100644 --- a/back/api/channels.js +++ b/back/api/channels.js @@ -11,14 +11,19 @@ const { getUserByUsername, deleteChannelMessages, deleteChannel, - getMessageReplies + getMessageReplies, + addAttachment, + getMessageAttachments, + getUnusedAttachments, + deleteUnusedAttachments } = require('../libs/mysql'); const rateLimit = require("express-rate-limit"); const slowDown = require("express-slow-down"); const { checkAuth } = require('../libs/middlewares'); const multer = require('multer'); +const fs = require('node:fs'); -const upload = multer({ dest: 'data/attachements/' }) +const upload = multer({ dest: 'data/attachments/' }) upload.limits = { fileSize: 1024 * 1024 * 5, files: 1, @@ -104,6 +109,15 @@ router.post('/:name/delete', async (req, res) => { } await deleteChannel(connection, channel[0].id); + + const attachments = await getUnusedAttachments(connection); + for (const attachment of attachments) { + if (fs.existsSync(`data/attachments/${attachment.file_name}`)) { + fs.unlinkSync(`data/attachments/${attachment.file_name}`); + } + } + await deleteUnusedAttachments(connection); + connection.end(); req.sockets.emit({ @@ -143,7 +157,7 @@ router.get('/:name/messages', async (req, res) => { const messages = await getMessages(connection, channel[0].id, limit); for (const message of messages) { - if (message.content.includes('@')) { + if (message.has_mentions) { const mentions = await getMentions(connection, message.id); message.mentions = mentions; } else { @@ -157,18 +171,28 @@ router.get('/:name/messages', async (req, res) => { else { message.replies = []; } + + if (message.has_attachments) { + const attachments = await getMessageAttachments(connection, message.id); + message.attachments = attachments; + } + else { + message.attachments = []; + } } connection.end(); res.send(messages); }); -router.post('/:name/messages/send', speedLimiter, limiter, upload.single("attachement"), checkAuth, async (req, res) => { +router.post('/:name/messages/send', speedLimiter, limiter, upload.single("attachment"), checkAuth, async (req, res) => { const { message } = req.body; const name = req.params.name; const user = req.user; + const attachement = req.file; if (!message) { + if (attachement) fs.unlinkSync(`data/attachements/${attachement.filename}`); return res.status(400).send({ error: 'Missing parameters' }); } @@ -177,12 +201,17 @@ router.post('/:name/messages/send', speedLimiter, limiter, upload.single("attach const channel = await getChannel(connection, name); if (!channel[0]) { connection.end(); + if (attachement) fs.unlinkSync(`data/attachements/${attachement.filename}`); return res.send('No channel found'); } const sent_message = await addMessage(connection, channel[0].id, user.id, message.replace("\"", "'")); const message_id = sent_message.insertId; + if (attachement) { + await addAttachment(connection, message_id, attachement.filename); + } + for (const word of message.split(' ')) { if (word.startsWith('@')) { const username = word.substring(1); diff --git a/back/api/lastmessages.js b/back/api/lastmessages.js index 27807f2..b4ffa08 100644 --- a/back/api/lastmessages.js +++ b/back/api/lastmessages.js @@ -1,5 +1,5 @@ const express = require('express'); -const { getConnection, getLastMessages, getMentions } = require('../libs/mysql'); +const { getConnection, getLastMessages, getMentions, getMessageAttachments } = require('../libs/mysql'); const router = express.Router(); @@ -15,6 +15,12 @@ router.get('/', async (req, res) => { message.mentions = []; } message.replies = []; + if (message.has_attachments) { + const attachments = await getMessageAttachments(connection, message.id); + message.attachments = attachments; + } else { + message.attachments = []; + } } connection.end(); diff --git a/back/api/messages.js b/back/api/messages.js index 9ef2db5..6433786 100644 --- a/back/api/messages.js +++ b/back/api/messages.js @@ -8,11 +8,14 @@ const { getMentions, getUserByUsername, addReply, - getMessageReplies + getMessageReplies, + getUnusedAttachments, + deleteUnusedAttachments } = require('../libs/mysql'); const rateLimit = require("express-rate-limit"); const slowDown = require("express-slow-down"); const { checkAuth } = require('../libs/middlewares'); +const fs = require('node:fs'); const limiter = rateLimit({ windowMs: 1 * 1000, @@ -132,6 +135,15 @@ router.post('/:message_id/delete', checkAuth, async (req, res) => { } await deleteMessage(connection, message_id); + + const attachments = await getUnusedAttachments(connection); + for (const attachment of attachments) { + if (fs.existsSync(`data/attachments/${attachment.file_name}`)) { + fs.unlinkSync(`data/attachments/${attachment.file_name}`); + } + } + await deleteUnusedAttachments(connection); + connection.end(); req.sockets.emit({ diff --git a/back/api/users.js b/back/api/users.js index f1ed9fd..06b71db 100644 --- a/back/api/users.js +++ b/back/api/users.js @@ -1,5 +1,5 @@ const express = require('express'); -const { getConnection, getUsers, getUserByUsername, getUserLastMessages, getMentions, deleteUser, deleteUserMessages, deleteUserMentions, setUserPfp } = require('../libs/mysql'); +const { getConnection, getUsers, getUserByUsername, getUserLastMessages, getMentions, deleteUser, setUserPfp, getMessageAttachments, getUnusedAttachments, deleteUnusedAttachments } = require('../libs/mysql'); const { checkAuth } = require("../libs/middlewares") const path = require('path'); const fs = require('node:fs'); @@ -38,6 +38,12 @@ router.get('/:username/lastmessages', async (req, res) => { message.mentions = []; } message.replies = []; + if (message.has_attachments) { + const attachments = await getMessageAttachments(connection, message.id); + message.attachments = attachments; + } else { + message.attachments = []; + } } connection.end(); @@ -87,6 +93,14 @@ router.post('/:username/delete', checkAuth, async (req, res) => { await deleteUser(connection, userToDelete[0].id); + const attachments = await getUnusedAttachments(connection); + for (const attachment of attachments) { + if (fs.existsSync(`data/attachments/${attachment.file_name}`)) { + fs.unlinkSync(`data/attachments/${attachment.file_name}`); + } + } + await deleteUnusedAttachments(connection); + connection.end(); req.sockets.emit({ diff --git a/back/libs/mysql.js b/back/libs/mysql.js index 4f48ab5..aeac65b 100644 --- a/back/libs/mysql.js +++ b/back/libs/mysql.js @@ -617,6 +617,20 @@ function deleteUnusedAttachments(connection) { }); } +function getUnusedAttachments(connection) { + return new Promise((resolve, reject) => { + connection.query( + `SELECT * FROM attachments WHERE message_id IS NULL`, + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + function getMessageAttachments(connection, message_id) { return new Promise((resolve, reject) => { connection.query( @@ -632,6 +646,21 @@ function getMessageAttachments(connection, message_id) { }); } +function getAttachment(connection, file_name) { + return new Promise((resolve, reject) => { + connection.query( + `SELECT * FROM attachments WHERE file_name = ?`, + [file_name], + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + module.exports = { getConnection, @@ -672,4 +701,10 @@ module.exports = { getEmoji, getEmojiByName, searchEmojis, + + addAttachment, + deleteUnusedAttachments, + getUnusedAttachments, + getMessageAttachments, + getAttachment, }; diff --git a/front/src/components/MessageComponent.tsx b/front/src/components/MessageComponent.tsx index 74afe8d..e38cc10 100644 --- a/front/src/components/MessageComponent.tsx +++ b/front/src/components/MessageComponent.tsx @@ -131,6 +131,13 @@ export default function MessageComponent({ message, user, channel }: { + {message.has_attachments == true && ( +
+ {message.attachments.map((attachment) => ( + attachment + ))} +
+ )}
{message.content.toLocaleLowerCase().includes("gros cochon") && ( Gros cochon diff --git a/front/src/pages/ChannelPage.tsx b/front/src/pages/ChannelPage.tsx index 504ce09..78793d6 100644 --- a/front/src/pages/ChannelPage.tsx +++ b/front/src/pages/ChannelPage.tsx @@ -23,8 +23,20 @@ export default function ChannelPage({socket}: {socket: WebSocket}) { function handleSubmit(e: React.FormEvent) { e.preventDefault(); + + const formData = new FormData(); + const fileInput = document.getElementById("attachment") as HTMLInputElement; + if (fileInput.files) { + formData.append("attachment", fileInput.files[0]); + } + formData.append("token", token); + formData.append("message", message); axios - .post(`/api/channels/${name}/messages/send`, { token, message}) + .post(`/api/channels/${name}/messages/send`, formData, { + headers: { + "Content-Type": "multipart/form-data", + } + }) .then(() => { setMessage(""); setSearchedUsers([]); @@ -204,6 +216,7 @@ export default function ChannelPage({socket}: {socket: WebSocket}) { onChange={(e) => setMessage(e.target.value)} ref={ref} /> + {searchedUsers.length > 0 && (
diff --git a/front/src/styles/MessageComponent.css b/front/src/styles/MessageComponent.css index 4a13b58..9db331a 100644 --- a/front/src/styles/MessageComponent.css +++ b/front/src/styles/MessageComponent.css @@ -42,6 +42,17 @@ max-height: 1em; } +.message-attachments { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; +} + +.message-attachment { + max-width: 100px; +} + .message-replies { width: 100%; display: flex; diff --git a/front/src/types.tsx b/front/src/types.tsx index 909f7a6..c042638 100644 --- a/front/src/types.tsx +++ b/front/src/types.tsx @@ -25,17 +25,27 @@ export type Mention = { export type Mentions = Mention[] +export type Attachment = { + id: number + message_id: number + file_name: string +} + +export type Attachments = Attachment[] + export type Message = { id: number user_id: number username: string content: string date: number - channel_id: number channel_name: string + has_mentions: boolean + has_replies: boolean + has_attachments: boolean mentions: Mentions replies: Messages - has_replies: boolean + attachments: Attachments } export type Messages = Message[]