Backend: MD5 Perceptual Hash Naming for Images
This commit is contained in:
parent
1202120d84
commit
28efa87729
888
backend/package-lock.json
generated
888
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,9 +23,9 @@
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-rate-limit": "^5.2.6",
|
||||
"faker": "^5.4.0",
|
||||
"get-stream": "^6.0.1",
|
||||
"got": "^11.7.0",
|
||||
"helmet": "^4.4.1",
|
||||
"image-hash": "^4.0.1",
|
||||
"imghash": "^0.0.9",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^5.10.7",
|
||||
|
@ -11,16 +11,16 @@ export default class Messages extends DBWrapper<string, MessageDocument> {
|
||||
return message;
|
||||
}
|
||||
|
||||
public async create(authorId: string, channelId: string, { attachmentIds, content }: Partial<Entity.Message>) {
|
||||
public async create(authorId: string, channelId: string, { attachmentURLs, content }: Partial<Entity.Message>) {
|
||||
if (!content)
|
||||
throw new TypeError('Content must be provided');
|
||||
// TODO: TESTME
|
||||
if (!content && !attachmentIds?.length)
|
||||
if (!content && !attachmentURLs?.length)
|
||||
throw new TypeError('Empty messages are not valid');
|
||||
|
||||
return await Message.create({
|
||||
_id: generateSnowflake(),
|
||||
attachmentIds,
|
||||
attachmentURLs,
|
||||
authorId,
|
||||
channelId,
|
||||
content,
|
||||
|
@ -14,7 +14,7 @@ export const Message = model<MessageDocument>('message', new Schema({
|
||||
type: String,
|
||||
default: generateSnowflake,
|
||||
},
|
||||
attachmentIds: [String],
|
||||
attachmentURLs: [String],
|
||||
authorId: {
|
||||
type: String,
|
||||
required: [true, 'Author ID is required'],
|
||||
|
@ -7,32 +7,36 @@ import { User } from '../../data/models/user';
|
||||
import rateLimiter from '../modules/rate-limiter';
|
||||
import multer from 'multer';
|
||||
import { generateSnowflake } from '../../data/snowflake-entity';
|
||||
import { imageHash } from 'image-hash';
|
||||
import path, { extname, resolve } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { APIError } from '../modules/api-error';
|
||||
import crypto from 'crypto';
|
||||
import getStream from 'get-stream';
|
||||
|
||||
function setupMulter(app: Application) {
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, fileMeta, cb) => cb(null, resolve('./assets/upload')),
|
||||
filename: async (req, fileMeta, cb) => {
|
||||
if (!fileMeta.mimetype.includes('image'))
|
||||
destination: (req, file, cb) => cb(null, resolve('./assets/upload')),
|
||||
filename: async (req, file, cb) => {
|
||||
if (!file.mimetype.includes('image'))
|
||||
throw new APIError(400, 'Only images can be uploaded at this time');
|
||||
|
||||
// const hash = promisify(imageHash);
|
||||
// const hashObj = await hash(file.buffer, 16, true) as object;
|
||||
|
||||
// console.log(hashObj);
|
||||
// console.log(file);
|
||||
|
||||
cb(null, Date.now() + extname(fileMeta.originalname));
|
||||
const buffer = await getStream(file.stream);
|
||||
const hash = crypto
|
||||
.createHash('md5')
|
||||
.update(buffer)
|
||||
.digest('hex');
|
||||
console.log(hash);
|
||||
|
||||
file['newName'] = hash + extname(file.originalname);
|
||||
cb(null, file['newName']);
|
||||
},
|
||||
});
|
||||
const upload = multer({ storage });
|
||||
|
||||
// TODO: validate is logged in, etc.
|
||||
app.post('/v2/upload', upload.single('file'), (req, res) => {
|
||||
res.status(201).json({ message: 'Files uploaded' });
|
||||
const fileName = req.file!['newName'];
|
||||
res.status(201).json({ url: `${process.env.ROOT_ENDPOINT}/assets/upload/${fileName}` });
|
||||
});
|
||||
}
|
||||
function setupPassport(app: Application) {
|
||||
|
@ -1,30 +1,23 @@
|
||||
import { Socket } from 'socket.io';
|
||||
import { WebSocket } from '../websocket';
|
||||
import { WSEvent, } from './ws-event';
|
||||
import { WSGuard } from '../modules/ws-guard';
|
||||
import { WS } from '../../types/ws';
|
||||
import { promisify } from 'util';
|
||||
import { stat } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { Channel } from '../../data/models/channel';
|
||||
|
||||
const statAsync = promisify(stat);
|
||||
|
||||
export default class implements WSEvent<'MESSAGE_CREATE'> {
|
||||
on = 'MESSAGE_CREATE' as const;
|
||||
|
||||
constructor(
|
||||
private messages = deps.messages,
|
||||
private guard = deps.wsGuard,
|
||||
private users = deps.users,
|
||||
) {}
|
||||
|
||||
public async invoke(ws: WebSocket, client: Socket, { attachmentIds, channelId, content, embed }: WS.Params.MessageCreate) {
|
||||
public async invoke(ws: WebSocket, client: Socket, { attachmentURLs, channelId, content, embed }: WS.Params.MessageCreate) {
|
||||
const authorId = ws.sessions.userId(client);
|
||||
|
||||
const [_, message, author] = await Promise.all([
|
||||
this.guard.validateCanInChannel(client, channelId, 'SEND_MESSAGES'),
|
||||
this.messages.create(authorId, channelId, { attachmentIds, content, embed }),
|
||||
this.users.getSelf(authorId),
|
||||
deps.wsGuard.validateCanInChannel(client, channelId, 'SEND_MESSAGES'),
|
||||
deps.messages.create(authorId, channelId, { attachmentURLs, content, embed }),
|
||||
deps.users.getSelf(authorId),
|
||||
]);
|
||||
|
||||
author.lastReadMessageIds ??= {};
|
||||
|
@ -43,9 +43,12 @@ const MessageContent: FunctionComponent<MessageContentProps> = ({ message }) =>
|
||||
.replace(patterns.blockQuoteMultiline, '<div class="blockquote pl-1">$1</div>')
|
||||
.replace(defaultPatterns.url, '<a href="$1" target="_blank">$1</div>');
|
||||
|
||||
// TODO: get metadata via fetch request, before rendering image
|
||||
const messageHTML =
|
||||
`${message.content && format(striptags(message.content))}` +
|
||||
`${message.attachments?.map(a => `<img src=${a.url} alt=${a.name} title=${a.name} />`)}`;
|
||||
`${message.attachmentURLs?.map(hash =>
|
||||
`<img src="${process.env.REACT_APP_CDN_URL}/uploads/${hash}"`)
|
||||
}`;
|
||||
|
||||
return (editingMessageId === message.id)
|
||||
? <MessageBox
|
||||
|
@ -17,7 +17,7 @@ export interface APIArgs {
|
||||
onSuccess?: string[];
|
||||
url: string;
|
||||
/** Callback to handle side effects. */
|
||||
callback?: (payload: any) => any;
|
||||
callback?: (payload: any) => any | Promise<any>;
|
||||
}
|
||||
export interface WSArgs {
|
||||
data?: object;
|
||||
|
@ -48,28 +48,26 @@ export const fetchMessages = (channelId: string) => (dispatch, getState: () => S
|
||||
}));
|
||||
}
|
||||
|
||||
export const createMessage = (channelId: string, payload: Partial<Entity.Message>, file?: File) => (dispatch) => {
|
||||
export const createMessage = (channelId: string, payload: Partial<Entity.Message>, attachmentURLs?: string[]) => (dispatch) => {
|
||||
dispatch(api.wsCallBegan({
|
||||
event: 'MESSAGE_CREATE',
|
||||
data: { ...payload, channelId, attachmentIds: [
|
||||
|
||||
] } as WS.Params.MessageCreate,
|
||||
data: { ...payload, channelId, attachmentURLs } as WS.Params.MessageCreate,
|
||||
}));
|
||||
}
|
||||
|
||||
// each file is uploaded individually as a separate API call
|
||||
export const uploadFileAsMessage = (channelId: string, payload: Partial<Entity.Message>, file: File) => (dispatch) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
formData.append('file', file);
|
||||
|
||||
dispatch(api.restCallBegan({
|
||||
method: 'post',
|
||||
url: '/upload',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
callback: async (resPayload: string[]) =>
|
||||
dispatch(createMessage(channelId, payload, resPayload)),
|
||||
}));
|
||||
|
||||
dispatch(createMessage(channelId, payload, file));
|
||||
}
|
||||
|
||||
export const updateMessage = (id: string, payload: Partial<Entity.Message>) => (dispatch) => {
|
||||
|
@ -2,7 +2,7 @@ import axios from 'axios';
|
||||
import { actions, APIArgs } from '../api';
|
||||
import { openDialog } from '../ui';
|
||||
|
||||
export default store => next => async action => {
|
||||
export default (store) => (next) => async (action) => {
|
||||
if (action.type !== actions.restCallBegan.type)
|
||||
return next(action);
|
||||
|
||||
@ -25,7 +25,7 @@ export default store => next => async action => {
|
||||
store.dispatch({ type, payload });
|
||||
|
||||
// called after dispatch events
|
||||
callback && callback(payload);
|
||||
callback && await callback(payload);
|
||||
} catch (error) {
|
||||
const response = (error as any).response;
|
||||
store.dispatch(actions.restCallFailed({ url, response }));
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { actions } from '../api';
|
||||
import ws from '../../services/ws-service';
|
||||
|
||||
export default store => next => async action => {
|
||||
export default (store) => (next) => async (action) => {
|
||||
if (action.type !== actions.wsCallBegan.type)
|
||||
return next(action);
|
||||
|
||||
|
2
types/entity.d.ts
vendored
2
types/entity.d.ts
vendored
@ -43,7 +43,7 @@ declare namespace Entity {
|
||||
}
|
||||
export interface Message {
|
||||
id: string;
|
||||
attachmentIds?: string[];
|
||||
attachmentURLs?: string[];
|
||||
authorId: string;
|
||||
channelId: string;
|
||||
content?: string;
|
||||
|
2
types/ws.d.ts
vendored
2
types/ws.d.ts
vendored
@ -194,7 +194,7 @@ declare namespace WS {
|
||||
export interface MessageCreate {
|
||||
channelId: string;
|
||||
content?: string;
|
||||
attachmentIds?: string[];
|
||||
attachmentURLs?: string[];
|
||||
embed?: MessageTypes.Embed;
|
||||
}
|
||||
export interface MessageDelete {
|
||||
|
Loading…
x
Reference in New Issue
Block a user