openvibers/openvibe.community Commits

A picture alone is a comment (#412)

OP OpenVibers committed 17 h ago · 5ed3a07562 · 3 files, +28 −9

A comment with a picture and no words was refused ("A comment is 1 to 2,000 characters"), unlike any social app (the owner's phone, 11-12). Words are now optional when a file is attached, on the comment form and in the comment service; with neither it still says what to do. The repeat check reads a picture-only comment by its files, so two different pictures are not "the same comment".

Changes

ddiff --git a/server/comments/form.js b/server/comments/form.js
iindex ecc3ed1..3d80cbb 100644
---- a/server/comments/form.js
++++ b/server/comments/form.js
@@@ -15,10 +15,11 @@ function parse(req, res, next) {
async function bodyFor(comments, viewer, ref, req) {
const body = req.body || {};
const message = String(body.message || '').trim();
-- if (!message || message.length > 2000) throw new ApiError(400, 'comment.message_invalid', 'A comment is 1 to 2,000 characters');
++ const files = req.files || [];
++ // A picture alone is a comment, as on any social app (the owner, 11-12): words are optional when a file is attached.
++ if ((!message && !files.length) || message.length > 2000) throw new ApiError(400, 'comment.message_invalid', 'Write something or attach a picture (up to 2,000 characters)');
const parent = String(body.parent_id || '');
if (parent && !/^\d{1,15}$/.test(parent)) throw new ApiError(400, 'comment.parent_invalid', 'That reply target is not valid');
-- const files = req.files || [];
const attachments = [];
for (const file of files) {
if (!TYPES.has(file.mimetype)) throw new ApiError(415, 'comment.file_type', 'Attach JPEG, PNG, GIF, WebP, PDF or text files');
ddiff --git a/server/comments/service.js b/server/comments/service.js
iindex bde7c15..6da5bf4 100644
---- a/server/comments/service.js
++++ b/server/comments/service.js
@@@ -204,9 +204,10 @@ function createCommentService({ db, network = null, limits = {}, canViewPaste =
return null;
}
-- function cleanMessage(raw) {
++ /** A comment's words. Empty only with files attached (a picture alone is a comment, 11-12). */
++ function cleanMessage(raw, { allowEmpty = false } = {}) {
const message = String(raw == null ? '' : raw).replace(/\u0000/g, '').trim();
-- if (!message) fail(400, 'comment.empty', 'Comment cannot be empty');
++ if (!message && !allowEmpty) fail(400, 'comment.empty', 'Write something or attach a picture');
if (message.length > MAX_MESSAGE) fail(400, 'comment.too_long', `Comment must be under ${MAX_MESSAGE} characters`);
return message;
}
@@@ -328,7 +329,8 @@ function createCommentService({ db, network = null, limits = {}, canViewPaste =
async add(v, id, body = {}) {
const t = await visibleThread(v, id);
if (t.visibility === 'locked' && !moderator(v)) fail(403, 'thread.locked', 'This comment thread is locked');
-- const message = cleanMessage(body.message);
++ const rawAttachments = body.attachments == null ? [] : (Array.isArray(body.attachments) ? body.attachments : [body.attachments]);
++ const message = cleanMessage(body.message, { allowEmpty: rawAttachments.length > 0 });
let parent = null;
let parentId = null;
@@@ -354,8 +356,9 @@ function createCommentService({ db, network = null, limits = {}, canViewPaste =
anonName = String(body.anon_name || '').trim().slice(0, 32).replace(/[^a-zA-Z0-9 _-]/g, '').trim() || 'Anonymous';
}
const key = author ? `s:${author}` : null;
-- commentLimiter.check(key, message);
-- const rawAttachments = body.attachments == null ? [] : (Array.isArray(body.attachments) ? body.attachments : [body.attachments]);
++ // The repeat check reads a picture-only comment by its files, so two different pictures are not "the same comment".
++ const said = message || `files:${JSON.stringify(rawAttachments).slice(0, 400)}`;
++ commentLimiter.check(key, said);
if (rawAttachments.length > 4) fail(400, 'comment.attachments_too_many', 'At most four files can be attached to a comment');
if (rawAttachments.length && !author) fail(401, 'auth.required', 'Sign in to attach a file');
let attachmentIds = [];
@@@ -377,7 +380,7 @@ function createCommentService({ db, network = null, limits = {}, canViewPaste =
const forumItem = forumLinks.BOARDS[`${t.ref_service}/${t.ref_type}`] ? await forumLinks.publicExternalItem(t, fetchImpl) : null;
const c = await store.insertComment(db, { thread_id: t.id, parent_id: parentId, reply_to_id: parent ? parent.id : null,
author_subject: author, anon_name: anonName, origin, message, attachment_ids: attachmentIds, forum_item: forumItem });
-- commentLimiter.record(key, message);
++ commentLimiter.record(key, said);
return { comment: (await shapeComments([c], v, t))[0] };
},
ddiff --git a/test/comments.test.js b/test/comments.test.js
iindex b0344c9..b325c14 100644
---- a/test/comments.test.js
++++ b/test/comments.test.js
@@@ -229,6 +229,21 @@ const { checkCapability } = require('../server/identity/capabilities');
assert.deepStrictEqual(after.comments, []);
assert.strictEqual((await t.db.prepare('SELECT COUNT(*) AS n FROM comment_attachments WHERE media_id = ?').get(attachment.id)).n, 0);
assert.ok(t.media.deletes.includes(attachment.id), 'removing the comment retires its Community Media object');
++
++ // A picture alone is a comment (the owner, 11-12: "it shouldn't require you to type text if you just put a
++ // picture"); with neither words nor a file it is still refused.
++ const form2 = new FormData();
++ form2.set('ref', JSON.stringify(ref));
++ form2.set('file', new Blob([Buffer.from('second picture')], { type: 'image/png' }), 'only.png');
++ const up2 = (await t.get('/api/v1/comments/attachments', { method: 'POST', headers: { authorization: `Bearer ${fileOwnerJwt}` }, body: form2 })).json().attachment;
++ const quiet = await post(thread.id, { message: '', attachments: [up2] }, { cookie: fileOwnerJwt });
++ assert.strictEqual(quiet.status, 201, quiet.text);
++ assert.deepStrictEqual([quiet.json().comment.message, quiet.json().comment.attachments.map((a) => a.id)], ['', [up2.id]], 'a picture with no caption');
++ const page2 = await t.get(`/c/${thread.id}`);
++ assert.match(page2.text, new RegExp(`<img src="https://openvibe\\.media/o/${up2.id}"`), 'and it shows');
++ const bare = await post(thread.id, { message: ' ' }, { cookie: fileOwnerJwt });
++ assert.strictEqual(bare.status, 400);
++ assert.strictEqual(bare.json().code, 'comment.empty');
});
await check('section reactions: one thread per post, the section id is the key; people only', async () => {
@@@ -545,7 +560,7 @@ const { checkCapability } = require('../server/identity/capabilities');
const form = (body, who) => t.get(`/c/${liveVod.access_id}`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', 'x-forwarded-for': ip2(), ...(who.origin ? { origin: who.origin } : {}) }, body, cookies: who.cookie ? [`ov_token=${who.cookie}`] : [] });
const empty = await form('message=+', { cookie: samJwt, origin: 'https://openvibe.community' });
assert.strictEqual(empty.status, 400);
-- assert.match(empty.text, /A comment is 1 to 2,000 characters/, 'the page explains the error');
++ assert.match(empty.text, /Write something or attach a picture/, 'the page explains the error');
const made = await form('message=said+on+Community', { cookie: samJwt });
assert.strictEqual(made.status, 303, made.text.slice(0, 200));
assert.match(made.headers.get('location'), new RegExp(`^/c/${liveVod.access_id}#comment-\\d+$`));