mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: #9109, ability to delete a post's diffs
This commit is contained in:
@@ -183,8 +183,10 @@
|
|||||||
"diffs.current-revision": "current revision",
|
"diffs.current-revision": "current revision",
|
||||||
"diffs.original-revision": "original revision",
|
"diffs.original-revision": "original revision",
|
||||||
"diffs.restore": "Restore this revision",
|
"diffs.restore": "Restore this revision",
|
||||||
"diffs.restore-description": "A new revision will be appended to this post's edit history.",
|
"diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.",
|
||||||
"diffs.post-restored": "Post successfully restored to earlier revision",
|
"diffs.post-restored": "Post successfully restored to earlier revision",
|
||||||
|
"diffs.delete": "Delete this revision",
|
||||||
|
"diffs.deleted": "Revision deleted",
|
||||||
|
|
||||||
"timeago_later": "%1 later",
|
"timeago_later": "%1 later",
|
||||||
"timeago_earlier": "%1 earlier",
|
"timeago_earlier": "%1 earlier",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) {
|
define('forum/topic/diffs', ['api', 'bootbox', 'forum/topic/images'], function (api, bootbox) {
|
||||||
var Diffs = {};
|
var Diffs = {};
|
||||||
|
|
||||||
Diffs.open = function (pid) {
|
Diffs.open = function (pid) {
|
||||||
@@ -8,51 +8,7 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
|
openModal(pid);
|
||||||
|
|
||||||
api.get(`/posts/${pid}/diffs`, {}).then((data) => {
|
|
||||||
app.parseAndTranslate('partials/modals/post_history', {
|
|
||||||
diffs: data.revisions.map(function (revision) {
|
|
||||||
var timestamp = parseInt(revision.timestamp, 10);
|
|
||||||
|
|
||||||
return {
|
|
||||||
username: revision.username,
|
|
||||||
timestamp: timestamp,
|
|
||||||
pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
numDiffs: data.timestamps.length,
|
|
||||||
editable: data.editable,
|
|
||||||
}, function (html) {
|
|
||||||
var modal = bootbox.dialog({
|
|
||||||
title: '[[topic:diffs.title]]',
|
|
||||||
message: html,
|
|
||||||
size: 'large',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data.timestamps.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectEl = modal.find('select');
|
|
||||||
var revertEl = modal.find('button[data-action="restore"]');
|
|
||||||
var postContainer = modal.find('ul.posts-list');
|
|
||||||
|
|
||||||
selectEl.on('change', function () {
|
|
||||||
Diffs.load(pid, this.value, postContainer);
|
|
||||||
revertEl.prop('disabled', data.timestamps.indexOf(this.value) === 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
revertEl.on('click', function () {
|
|
||||||
Diffs.restore(pid, selectEl.val(), modal);
|
|
||||||
});
|
|
||||||
|
|
||||||
modal.on('shown.bs.modal', function () {
|
|
||||||
Diffs.load(pid, selectEl.val(), postContainer);
|
|
||||||
revertEl.prop('disabled', true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).catch(app.alertError);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Diffs.load = function (pid, since, postContainer) {
|
Diffs.load = function (pid, since, postContainer) {
|
||||||
@@ -82,5 +38,77 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) {
|
|||||||
}).catch(app.alertError);
|
}).catch(app.alertError);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Diffs.delete = function (pid, timestamp, modal) {
|
||||||
|
api.del(`/posts/${pid}/diffs/${timestamp}`).then(() => {
|
||||||
|
openModal(pid, modal);
|
||||||
|
app.alertSuccess('[[topic:diffs.deleted]]');
|
||||||
|
}).catch(app.alertError);
|
||||||
|
};
|
||||||
|
|
||||||
|
function openModal(pid, modal) {
|
||||||
|
var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
|
||||||
|
|
||||||
|
api.get(`/posts/${pid}/diffs`, {}).then((data) => {
|
||||||
|
app.parseAndTranslate('partials/modals/post_history', {
|
||||||
|
diffs: data.revisions.map(function (revision) {
|
||||||
|
var timestamp = parseInt(revision.timestamp, 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: revision.username,
|
||||||
|
timestamp: timestamp,
|
||||||
|
pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
numDiffs: data.timestamps.length,
|
||||||
|
editable: data.editable,
|
||||||
|
deletable: data.deletable,
|
||||||
|
}, function (html) {
|
||||||
|
const modalExists = !!modal;
|
||||||
|
if (modalExists) {
|
||||||
|
modal.find('.modal-body').html(html);
|
||||||
|
} else {
|
||||||
|
modal = bootbox.dialog({
|
||||||
|
title: '[[topic:diffs.title]]',
|
||||||
|
message: html,
|
||||||
|
size: 'large',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.timestamps.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectEl = modal.find('select');
|
||||||
|
var revertEl = modal.find('button[data-action="restore"]');
|
||||||
|
var deleteEl = modal.find('button[data-action="delete"]');
|
||||||
|
var postContainer = modal.find('ul.posts-list');
|
||||||
|
|
||||||
|
selectEl.on('change', function () {
|
||||||
|
Diffs.load(pid, this.value, postContainer);
|
||||||
|
revertEl.prop('disabled', data.timestamps.indexOf(this.value) === 0);
|
||||||
|
deleteEl.prop('disabled', data.timestamps.indexOf(this.value) === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
revertEl.on('click', function () {
|
||||||
|
Diffs.restore(pid, selectEl.val(), modal);
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteEl.on('click', function () {
|
||||||
|
Diffs.delete(pid, selectEl.val(), modal);
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.on('shown.bs.modal', function () {
|
||||||
|
Diffs.load(pid, selectEl.val(), postContainer);
|
||||||
|
revertEl.prop('disabled', true);
|
||||||
|
deleteEl.prop('disabled', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modalExists) {
|
||||||
|
modal.trigger('shown.bs.modal');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(app.alertError);
|
||||||
|
}
|
||||||
|
|
||||||
return Diffs;
|
return Diffs;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -260,9 +260,14 @@ postsAPI.getDiffs = async (caller, data) => {
|
|||||||
let usernames = await user.getUsersFields(uids, ['username']);
|
let usernames = await user.getUsersFields(uids, ['username']);
|
||||||
usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null));
|
usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null));
|
||||||
|
|
||||||
|
const cid = await posts.getCidByPid(data.pid);
|
||||||
|
const isModerator = await privileges.users.isModerator(cid, caller.uid);
|
||||||
|
|
||||||
let canEdit = true;
|
let canEdit = true;
|
||||||
try {
|
try {
|
||||||
|
if (!isModerator) {
|
||||||
await user.isPrivilegedOrSelf(caller.uid, post.uid);
|
await user.isPrivilegedOrSelf(caller.uid, post.uid);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
canEdit = false;
|
canEdit = false;
|
||||||
}
|
}
|
||||||
@@ -276,6 +281,7 @@ postsAPI.getDiffs = async (caller, data) => {
|
|||||||
username: usernames[idx],
|
username: usernames[idx],
|
||||||
})),
|
})),
|
||||||
editable: canEdit,
|
editable: canEdit,
|
||||||
|
deletable: isModerator,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const posts = require('../../posts');
|
const posts = require('../../posts');
|
||||||
|
const privileges = require('../../privileges');
|
||||||
|
|
||||||
const api = require('../../api');
|
const api = require('../../api');
|
||||||
const helpers = require('../helpers');
|
const helpers = require('../helpers');
|
||||||
@@ -94,3 +95,19 @@ Posts.restoreDiff = async (req, res) => {
|
|||||||
helpers.formatApiResponse(200, res, await api.posts.restoreDiff(req, { ...req.params }));
|
helpers.formatApiResponse(200, res, await api.posts.restoreDiff(req, { ...req.params }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Posts.deleteDiff = async (req, res) => {
|
||||||
|
if (!parseInt(req.params.pid, 10)) {
|
||||||
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cid = await posts.getCidByPid(req.params.pid);
|
||||||
|
const isModerator = privileges.users.isModerator(cid, req.uid);
|
||||||
|
|
||||||
|
if (!isModerator) {
|
||||||
|
return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await posts.diffs.delete(req.params.pid, req.params.timestamp, req.uid);
|
||||||
|
|
||||||
|
helpers.formatApiResponse(200, res);
|
||||||
|
};
|
||||||
|
|||||||
@@ -71,28 +71,79 @@ module.exports = function (Posts) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
async function postDiffLoad(pid, since, uid) {
|
Diffs.delete = async function (pid, timestamp, uid) {
|
||||||
// Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since`
|
getValidatedTimestamp(timestamp);
|
||||||
since = parseInt(since, 10);
|
|
||||||
|
|
||||||
if (isNaN(since) || since > Date.now()) {
|
const [post, diffs, timestamps] = await Promise.all([
|
||||||
|
Posts.getPostSummaryByPids([pid], uid, { parse: false }),
|
||||||
|
Diffs.get(pid),
|
||||||
|
Diffs.list(pid),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const timestampIndex = timestamps.indexOf(timestamp);
|
||||||
|
const lastTimestampIndex = timestamps.length - 1;
|
||||||
|
|
||||||
|
if (timestamp === String(post[0].timestamp)) {
|
||||||
|
return Promise.all([
|
||||||
|
db.delete(`diff:${pid}.${timestamps[lastTimestampIndex]}`),
|
||||||
|
db.listRemoveAll(`post:${pid}:diffs`, timestamps[lastTimestampIndex]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (timestampIndex === 0 || timestampIndex === -1) {
|
||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postContent = validator.unescape(post[0].content);
|
||||||
|
const versionContents = {};
|
||||||
|
for (let i = 0, content = postContent; i < timestamps.length; ++i) {
|
||||||
|
versionContents[timestamps[i]] = applyPatch(content, diffs[i]);
|
||||||
|
content = versionContents[timestamps[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
for (let i = lastTimestampIndex; i >= timestampIndex; --i) {
|
||||||
|
const newContentIndex = i === timestampIndex ? i - 2 : i - 1;
|
||||||
|
const timestampToUpdate = newContentIndex + 1;
|
||||||
|
const newContent = newContentIndex < 0 ? postContent : versionContents[timestamps[newContentIndex]];
|
||||||
|
const patch = diff.createPatch('', newContent, versionContents[timestamps[i]]);
|
||||||
|
await db.setObject('diff:' + pid + '.' + timestamps[timestampToUpdate], { patch });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
db.delete(`diff:${pid}.${timestamp}`),
|
||||||
|
db.listRemoveAll(`post:${pid}:diffs`, timestamp),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function postDiffLoad(pid, since, uid) {
|
||||||
|
// Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since`
|
||||||
|
since = getValidatedTimestamp(since);
|
||||||
|
|
||||||
const [post, diffs] = await Promise.all([
|
const [post, diffs] = await Promise.all([
|
||||||
Posts.getPostSummaryByPids([pid], uid, { parse: false }),
|
Posts.getPostSummaryByPids([pid], uid, { parse: false }),
|
||||||
Posts.diffs.get(pid, since),
|
Posts.diffs.get(pid, since),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Replace content with re-constructed content from that point in time
|
// Replace content with re-constructed content from that point in time
|
||||||
post[0].content = diffs.reduce(function (content, currentDiff) {
|
post[0].content = diffs.reduce(applyPatch, validator.unescape(post[0].content));
|
||||||
const result = diff.applyPatch(content, currentDiff.patch, {
|
|
||||||
fuzzFactor: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return typeof result === 'string' ? result : content;
|
|
||||||
}, validator.unescape(post[0].content));
|
|
||||||
|
|
||||||
return post[0];
|
return post[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getValidatedTimestamp(timestamp) {
|
||||||
|
timestamp = parseInt(timestamp, 10);
|
||||||
|
|
||||||
|
if (isNaN(timestamp) || timestamp > Date.now()) {
|
||||||
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatch(content, aDiff) {
|
||||||
|
const result = diff.applyPatch(content, aDiff.patch, {
|
||||||
|
fuzzFactor: 1,
|
||||||
|
});
|
||||||
|
return typeof result === 'string' ? result : content;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ module.exports = function () {
|
|||||||
setupApiRoute(router, 'get', '/:pid/diffs', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.getDiffs);
|
setupApiRoute(router, 'get', '/:pid/diffs', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.getDiffs);
|
||||||
setupApiRoute(router, 'get', '/:pid/diffs/:since', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.loadDiff);
|
setupApiRoute(router, 'get', '/:pid/diffs/:since', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.loadDiff);
|
||||||
setupApiRoute(router, 'put', '/:pid/diffs/:since', [...middlewares, middleware.assert.post], controllers.write.posts.restoreDiff);
|
setupApiRoute(router, 'put', '/:pid/diffs/:since', [...middlewares, middleware.assert.post], controllers.write.posts.restoreDiff);
|
||||||
|
setupApiRoute(router, 'delete', '/:pid/diffs/:timestamp', [...middlewares, middleware.assert.post], controllers.write.posts.deleteDiff);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user