nodebb-plugin-moving-topics 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/library.js +124 -0
- package/package.json +7 -0
- package/plugin.json +14 -0
- package/static/lib/client.js +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# NodeBB Plugin: Moving Topics for Owners
|
|
2
|
+
|
|
3
|
+
Allow topic owners to move their own topics between categories, without granting full moderation privileges.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Adds **Move** to the thread tools for the topic owner.
|
|
8
|
+
- Owners can move **only their own topics**.
|
|
9
|
+
- Moves are blocked for **locked** or **deleted** topics.
|
|
10
|
+
- Target categories are filtered by `topics:create` + `topics:read`.
|
|
11
|
+
- Admins/moderators retain existing move permissions.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- NodeBB (compatible with current core API used by this plugin)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Via NodeBB CLI
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
cd /path/to/nodebb
|
|
23
|
+
./nodebb install nodebb-plugin-moving-topics
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Manual
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
cd /path/to/nodebb
|
|
30
|
+
npm install nodebb-plugin-moving-topics
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then rebuild and restart:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
./nodebb build
|
|
37
|
+
./nodebb restart
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
No settings are required. The plugin respects existing category permissions.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
For topic owners, the **Move** option appears in the thread tools menu. Choose a destination category and confirm.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/library.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const plugin = {};
|
|
4
|
+
|
|
5
|
+
let patchedMove = false;
|
|
6
|
+
let originalMove;
|
|
7
|
+
|
|
8
|
+
function getCore(path) {
|
|
9
|
+
return require.main.require(path);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
plugin.init = async function () {
|
|
13
|
+
if (patchedMove) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
patchedMove = true;
|
|
17
|
+
|
|
18
|
+
const topicsApi = getCore('./src/api/topics');
|
|
19
|
+
const privileges = getCore('./src/privileges');
|
|
20
|
+
const topics = getCore('./src/topics');
|
|
21
|
+
const categories = getCore('./src/categories');
|
|
22
|
+
const batch = getCore('./src/batch');
|
|
23
|
+
const socketHelpers = getCore('./src/socket.io/helpers');
|
|
24
|
+
const events = getCore('./src/events');
|
|
25
|
+
const activitypubApi = getCore('./src/api/activitypub');
|
|
26
|
+
const activitypub = getCore('./src/activitypub');
|
|
27
|
+
const user = getCore('./src/user');
|
|
28
|
+
|
|
29
|
+
originalMove = topicsApi.move;
|
|
30
|
+
|
|
31
|
+
topicsApi.move = async function (caller, data) {
|
|
32
|
+
const canMove = await privileges.categories.isAdminOrMod(data.cid, caller.uid);
|
|
33
|
+
if (canMove) {
|
|
34
|
+
return await originalMove(caller, data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tids = Array.isArray(data.tid) ? data.tid : [data.tid];
|
|
38
|
+
const targetCid = parseInt(data.cid, 10);
|
|
39
|
+
|
|
40
|
+
const [canCreate, canRead] = await Promise.all([
|
|
41
|
+
privileges.categories.can('topics:create', targetCid, caller.uid),
|
|
42
|
+
privileges.categories.can('topics:read', targetCid, caller.uid),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
if (!canCreate || !canRead) {
|
|
46
|
+
throw new Error('[[error:no-privileges]]');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const uids = await user.getUidsFromSet('users:online', 0, -1);
|
|
50
|
+
const cids = [targetCid];
|
|
51
|
+
|
|
52
|
+
await batch.processArray(tids, async (batchTids) => {
|
|
53
|
+
await Promise.all(batchTids.map(async (tid) => {
|
|
54
|
+
const topicData = await topics.getTopicFields(tid, [
|
|
55
|
+
'tid',
|
|
56
|
+
'cid',
|
|
57
|
+
'uid',
|
|
58
|
+
'mainPid',
|
|
59
|
+
'slug',
|
|
60
|
+
'deleted',
|
|
61
|
+
'locked',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
if (!topicData) {
|
|
65
|
+
throw new Error('[[error:no-topic]]');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isOwner = parseInt(topicData.uid, 10) === parseInt(caller.uid, 10);
|
|
69
|
+
if (!isOwner || topicData.locked || topicData.deleted) {
|
|
70
|
+
throw new Error('[[error:no-privileges]]');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!cids.includes(topicData.cid)) {
|
|
74
|
+
cids.push(topicData.cid);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await topics.tools.move(tid, {
|
|
78
|
+
cid: targetCid,
|
|
79
|
+
uid: caller.uid,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const notifyUids = await privileges.categories.filterUids('topics:read', topicData.cid, uids);
|
|
83
|
+
socketHelpers.emitToUids('event:topic_moved', topicData, notifyUids);
|
|
84
|
+
|
|
85
|
+
if (!topicData.deleted) {
|
|
86
|
+
socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic');
|
|
87
|
+
activitypubApi.announce.note(caller, { tid });
|
|
88
|
+
const { activity } = await activitypub.mocks.activities.create(topicData.mainPid, caller.uid);
|
|
89
|
+
await activitypub.feps.announce(topicData.mainPid, activity);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await events.log({
|
|
93
|
+
type: 'topic-move',
|
|
94
|
+
uid: caller.uid,
|
|
95
|
+
ip: caller.ip,
|
|
96
|
+
tid: tid,
|
|
97
|
+
fromCid: topicData.cid,
|
|
98
|
+
toCid: targetCid,
|
|
99
|
+
});
|
|
100
|
+
}));
|
|
101
|
+
}, { batch: 10 });
|
|
102
|
+
|
|
103
|
+
await categories.onTopicsMoved(cids);
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
plugin.filterTopicPrivileges = async function (data) {
|
|
108
|
+
const topics = getCore('./src/topics');
|
|
109
|
+
const topicData = await topics.getTopicFields(data.tid, ['uid', 'locked', 'deleted']);
|
|
110
|
+
if (!topicData) {
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isOwner = parseInt(topicData.uid, 10) === parseInt(data.uid, 10);
|
|
115
|
+
data.canMoveOwnTopic = !!(isOwner && !data.isAdminOrMod && !topicData.locked && !topicData.deleted);
|
|
116
|
+
|
|
117
|
+
if (data.canMoveOwnTopic && !data.view_thread_tools) {
|
|
118
|
+
data.view_thread_tools = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return data;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
module.exports = plugin;
|
package/package.json
ADDED
package/plugin.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-moving-topics",
|
|
3
|
+
"name": "Moving Topics for Owners",
|
|
4
|
+
"description": "Allow topic owners to move their own topics.",
|
|
5
|
+
"url": "https://github.com/palmoni5/nodebb-plugin-moving-topics",
|
|
6
|
+
"library": "library.js",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
9
|
+
{ "hook": "filter:privileges.topics.get", "method": "filterTopicPrivileges" }
|
|
10
|
+
],
|
|
11
|
+
"scripts": [
|
|
12
|
+
"static/lib/client.js"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* global ajaxify */
|
|
4
|
+
|
|
5
|
+
$(window).on('action:app.load', function () {
|
|
6
|
+
require(['hooks', 'translator'], function (hooks, translator) {
|
|
7
|
+
function canMoveOwnTopic() {
|
|
8
|
+
const privs = ajaxify.data && ajaxify.data.privileges;
|
|
9
|
+
return !!(privs && privs.canMoveOwnTopic);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function addMoveItem(menuEl) {
|
|
13
|
+
const $menu = $(menuEl);
|
|
14
|
+
if ($menu.find('.plugin-move-own-topic').length) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
translator.translate('[[topic:thread-tools.move]]', function (label) {
|
|
19
|
+
const item = [
|
|
20
|
+
'<li>',
|
|
21
|
+
'<a href="#" class="dropdown-item rounded-1 d-flex align-items-center gap-2 plugin-move-own-topic" role="menuitem">',
|
|
22
|
+
'<i class="fa fa-fw fa-arrows text-secondary"></i> ',
|
|
23
|
+
label,
|
|
24
|
+
'</a>',
|
|
25
|
+
'</li>',
|
|
26
|
+
].join('');
|
|
27
|
+
$menu.append(item);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function bindMoveHandler(menuEl) {
|
|
32
|
+
const $menu = $(menuEl);
|
|
33
|
+
$menu.off('click', '.plugin-move-own-topic').on('click', '.plugin-move-own-topic', function () {
|
|
34
|
+
require(['forum/topic/move'], function (move) {
|
|
35
|
+
move.init([ajaxify.data.tid], ajaxify.data.cid);
|
|
36
|
+
});
|
|
37
|
+
return false;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
hooks.on('action:topic.tools.load', function (data) {
|
|
42
|
+
if (!canMoveOwnTopic()) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
addMoveItem(data.element);
|
|
46
|
+
bindMoveHandler(data.element);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
hooks.on('action:category.selector.options', function (data) {
|
|
50
|
+
if (!canMoveOwnTopic()) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const el = data.el;
|
|
54
|
+
if (!el || !el.length) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const inMoveModal = el.closest('.tool-modal').find('#move_thread_commit').length > 0;
|
|
58
|
+
if (inMoveModal) {
|
|
59
|
+
data.options.privilege = 'topics:create';
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|